diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
deleted file mode 100644
index 432a85b..0000000
--- a/.config/dotnet-tools.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "version": 1,
- "isRoot": true,
- "tools": {
- "csharpier": {
- "version": "1.2.6",
- "commands": [
- "csharpier"
- ]
- }
- }
-}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..6edad4b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+# EditorConfig (https://editorconfig.org) — Beanfun
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Rust / TOML follow Rust community convention.
+[*.{rs,toml}]
+indent_size = 4
+
+# Markdown allows trailing whitespace (two-space line break).
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1e16023
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,10 @@
+# Force LF line endings for all text files, regardless of OS.
+# Prevents CRLF issues in CI (rustfmt, prettier) on Windows runners.
+* text=auto eol=lf
+
+# Ensure these are always treated as binary (no line-ending conversion).
+*.png binary
+*.ico binary
+*.dll binary
+*.exe binary
+*.xml binary
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9d0c4ac
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: cargo
+ directory: /src-tauri
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml
index 4a41e50..9d6a803 100644
--- a/.github/workflows/build-and-release.yml
+++ b/.github/workflows/build-and-release.yml
@@ -4,21 +4,21 @@ on:
workflow_dispatch:
inputs:
release_name:
- description: "Release Name (leave empty to auto-generate)"
+ description: 'Release Name (leave empty to auto-generate)'
required: false
- default: ""
+ default: ''
release_type:
- description: "Release type"
+ description: 'Release type'
required: true
- default: "prerelease"
+ default: 'prerelease'
type: choice
options:
- prerelease
- release
version_increment:
- description: "Version increment type"
+ description: 'Version increment type'
required: false
- default: "patch"
+ default: 'patch'
type: choice
options:
- patch
@@ -30,47 +30,47 @@ env:
jobs:
build:
+ name: 'Build Beanfun.exe & Create Release'
runs-on: windows-latest
permissions:
contents: write
steps:
- - name: Checkout Code
+ - name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
with:
- dotnet-version: "8.0.x"
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: package-lock.json
- - name: Prepare Version & Inject AssemblyInfo
+ - name: Install Rust (stable)
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: src-tauri
+
+ - name: Install frontend dependencies
+ run: npm ci
+
+ - name: Prepare version
id: prep_version
shell: pwsh
run: |
- $file = Get-ChildItem -Path . -Filter "AssemblyInfo.cs" -Recurse | Select-Object -First 1
- if (-not $file) { Write-Error "AssemblyInfo.cs not found!"; exit 1 }
-
- $content = Get-Content $file.FullName -Raw
- if ($content -match 'AssemblyVersion\("(\d+)\.(\d+)(?:\.\d+)?\.\*"\)') {
+ $cargoToml = Get-Content src-tauri/Cargo.toml -Raw
+ if ($cargoToml -match 'version\s*=\s*"(\d+)\.(\d+)\.(\d+)"') {
$major = [int]$matches[1]
$minor = [int]$matches[2]
- } else {
- $major = 5
- $minor = 8
- }
-
- git fetch --tags 2>$null
- $tags = git tag -l "v$major.$minor.*.*" --sort=-v:refname
-
- $patch = 0
- $tagsArray = @($tags)
- if ($tagsArray.Count -gt 0 -and $tagsArray[0]) {
- $latestTag = $tagsArray[0]
- if ($latestTag -match "^v\d+\.\d+\.(\d+)\.") {
- $patch = [int]$matches[1]
- }
+ $patch = [int]$matches[3]
+ } else {
+ Write-Error "Could not parse version from Cargo.toml"
+ exit 1
}
$releaseType = "${{ github.event.inputs.release_type }}"
@@ -82,44 +82,50 @@ jobs:
'minor' { $minor++; $patch=0 }
'patch' { $patch++ }
}
- } else {
- $patch++
+ } else {
+ $patch++
}
$utcNow = (Get-Date).ToUniversalTime()
$timestamp = $utcNow.ToString("yyMMddHHmm")
$buildDateTime = $utcNow.ToString("yyyy-MM-dd HH:mm:ss")
- $baseDate = (Get-Date -Year 2000 -Month 1 -Day 1).ToUniversalTime()
- $build = [math]::Floor(($utcNow - $baseDate).TotalDays)
- $revision = [math]::Floor($utcNow.TimeOfDay.TotalSeconds / 2)
-
- $infoVersion = "$major.$minor.$patch($timestamp)"
- $tagName = "v$major.$minor.$patch.$timestamp"
$semVer = "$major.$minor.$patch"
+ $infoVersion = "$semVer($timestamp)"
+ $tagName = "v$semVer.$timestamp"
- Write-Host "🚀 New Version: $infoVersion"
-
- $content = $content -replace 'AssemblyVersion\(".*?"\)', "AssemblyVersion(`"$major.$minor.*`")"
- if ($content -match 'AssemblyInformationalVersion') {
- $content = $content -replace 'AssemblyInformationalVersion\(".*?"\)', "AssemblyInformationalVersion(`"$infoVersion`")"
- } else {
- $content += "`r`n[assembly: System.Reflection.AssemblyInformationalVersion(`"$infoVersion`")]"
- }
- if (-not $content.EndsWith("`n")) { $content += "`r`n" }
- $content | Set-Content $file.FullName -NoNewline
+ Write-Host "Version: $infoVersion"
"tag_name=$tagName" >> $env:GITHUB_OUTPUT
"semantic_version=$semVer" >> $env:GITHUB_OUTPUT
"info_version=$infoVersion" >> $env:GITHUB_OUTPUT
"build_datetime=$buildDateTime" >> $env:GITHUB_OUTPUT
- "build=$build" >> $env:GITHUB_OUTPUT
- "revision=$revision" >> $env:GITHUB_OUTPUT
- - name: Publish Application
- run: dotnet publish Beanfun/Beanfun.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeAllContentForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=none -p:InformationalVersion="${{ steps.prep_version.outputs.info_version }}" -o publish
+ - name: Update version in Cargo.toml, tauri.conf.json, package.json
+ shell: pwsh
+ run: |
+ $semVer = "${{ steps.prep_version.outputs.semantic_version }}"
+
+ $cargo = Get-Content src-tauri/Cargo.toml -Raw -Encoding UTF8
+ $cargo = $cargo -replace '(?m)^version\s*=\s*"[\d.]+"', "version = `"$semVer`""
+ [System.IO.File]::WriteAllText("src-tauri/Cargo.toml", $cargo)
+
+ $tauri = Get-Content src-tauri/tauri.conf.json -Raw -Encoding UTF8
+ $tauri = $tauri -replace '"version"\s*:\s*"[\d.]+"', "`"version`": `"$semVer`""
+ [System.IO.File]::WriteAllText("src-tauri/tauri.conf.json", $tauri)
+
+ $pkg = Get-Content package.json -Raw -Encoding UTF8
+ $pkg = $pkg -replace '"version"\s*:\s*"[\d.]+"', "`"version`": `"$semVer`""
+ [System.IO.File]::WriteAllText("package.json", $pkg)
+
+ - name: Build Tauri application (standalone exe)
+ run: npm run tauri build -- --no-bundle
- - name: Prepare Release Notes
+ - name: Rename exe to Beanfun.exe
+ shell: cmd
+ run: ren src-tauri\target\release\beanfun.exe Beanfun.exe
+
+ - name: Prepare release notes
id: prep_release
shell: pwsh
env:
@@ -132,16 +138,22 @@ jobs:
"exists=$($exists.ToString().ToLower())" >> $env:GITHUB_OUTPUT
if ($exists) {
- Write-Host "⚠️ Tag $tag already exists. Skipping release creation."
+ Write-Host "Tag $tag already exists. Skipping release creation."
exit 0
}
- $lastRelease = gh api repos/${{ github.repository }}/releases --paginate 2>$null | ConvertFrom-Json | Sort-Object created_at -Descending | Select-Object -First 1
- $range = if ($lastRelease) { "$($lastRelease.tag_name)..HEAD" } else { "-n 30" }
-
$repoUrl = "${{ github.server_url }}/${{ github.repository }}"
- $commits = git log $range --pretty=format:"- [%h]($repoUrl/commit/%H) %s - %an (%ad)" --date=short --no-merges
+ $lastRelease = gh api repos/${{ github.repository }}/releases --paginate 2>$null | ConvertFrom-Json | Sort-Object created_at -Descending | Select-Object -First 1
+
+ $logArgs = @('log', '--pretty=format:"- [%h](' + $repoUrl + '/commit/%H) %s - %an (%ad)"', '--date=short', '--no-merges')
+ if ($lastRelease) {
+ $logArgs += "$($lastRelease.tag_name)..HEAD"
+ } else {
+ $logArgs += '-n'
+ $logArgs += '30'
+ }
+ $commits = & git @logArgs
$commitCount = if ($commits) { ($commits -split "`n").Count } else { 0 }
$commitsText = if ($commits) { $commits -join "`n" } else { "No commits found" }
@@ -152,15 +164,15 @@ jobs:
$delimiter >> $env:GITHUB_OUTPUT
"commit_count=$commitCount" >> $env:GITHUB_OUTPUT
- - name: Create Release
+ - name: Create GitHub Release
if: steps.prep_release.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.prep_version.outputs.tag_name }}
name: >
- ${{
- github.event.inputs.release_name != ''
- && github.event.inputs.release_name
+ ${{
+ github.event.inputs.release_name != ''
+ && github.event.inputs.release_name
|| format('v{0}', steps.prep_version.outputs.semantic_version)
}}
body: |
@@ -191,21 +203,19 @@ jobs:
|------|-------|
| Full Build Version | ${{ steps.prep_version.outputs.info_version }} |
| Build Date/Time (UTC) | ${{ steps.prep_version.outputs.build_datetime }} |
- | MS Build Revision | ${{ steps.prep_version.outputs.build }}.${{ steps.prep_version.outputs.revision }} |
| Source Commit | [View](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) |
- files: publish/Beanfun.exe
+ files: src-tauri/target/release/Beanfun.exe
draft: false
prerelease: ${{ github.event.inputs.release_type == 'prerelease' }}
- - name: Commit Version Change
+ - name: Commit version bump
if: steps.prep_release.outputs.exists == 'false'
shell: pwsh
run: |
- $file = Get-ChildItem -Path . -Filter "AssemblyInfo.cs" -Recurse | Select-Object -First 1
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git add $file.FullName
+ git add src-tauri/Cargo.toml src-tauri/tauri.conf.json package.json
git commit -m "Bump version to ${{ steps.prep_version.outputs.semantic_version }} for release ${{ steps.prep_version.outputs.tag_name }}"
git push
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fb51e93
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,71 @@
+name: Lint, Format & Test
+
+on:
+ push:
+ branches: [code]
+ pull_request:
+ branches: [code]
+ workflow_dispatch:
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ frontend:
+ name: 'Frontend: ESLint / Prettier / TypeCheck / Vitest'
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: package-lock.json
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: ESLint
+ run: npm run lint
+
+ - name: Prettier check
+ run: npm run format:check
+
+ - name: Type check
+ run: npm run typecheck
+
+ - name: Unit tests (Vitest)
+ run: npm run test
+
+ rust:
+ name: 'Rust: fmt / Clippy / Test'
+ runs-on: windows-latest
+ defaults:
+ run:
+ working-directory: src-tauri
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Rust (stable)
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - name: Cache Cargo
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: src-tauri
+
+ - name: cargo fmt
+ run: cargo fmt --all -- --check
+
+ - name: cargo clippy
+ run: cargo clippy --all-targets -- -D warnings
+
+ - name: cargo test
+ run: cargo test
diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml
deleted file mode 100644
index cd8281d..0000000
--- a/.github/workflows/format-check.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Format Check
-
-on:
- push:
- branches: [code]
- pull_request:
- branches: [code]
-
-jobs:
- format:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout Code
- uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 8.x
-
- - name: Restore Tools
- run: dotnet tool restore
-
- - name: Check Formatting
- run: dotnet csharpier check .
diff --git a/.gitignore b/.gitignore
index d294810..7b78b08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,277 +1,42 @@
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-
-# User-specific files
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# User-specific files (MonoDevelop/Xamarin Studio)
-*.userprefs
-
-# Build results
-[Dd]ebug/
-[Dd]ebugPublic/
-[Rr]elease/
-[Rr]eleases/
-x64/
-x86/
-bld/
-[Bb]in/
-[Oo]bj/
-[Ll]og/
-
-# Visual Studio 2015 cache/options directory
-.vs/
-# Uncomment if you have tasks that create the project's static files in wwwroot
-#wwwroot/
-
-# MSTest test Results
-[Tt]est[Rr]esult*/
-[Bb]uild[Ll]og.*
-
-# NUNIT
-*.VisualState.xml
-TestResult.xml
-
-# Build Results of an ATL Project
-[Dd]ebugPS/
-[Rr]eleasePS/
-dlldata.c
-
-# DNX
-project.lock.json
-project.fragment.lock.json
-artifacts/
-
-*_i.c
-*_p.c
-*_i.h
-*.ilk
-*.meta
-*.obj
-*.pch
-*.pdb
-*.pgc
-*.pgd
-*.rsp
-*.sbr
-*.tlb
-*.tli
-*.tlh
-*.tmp
-*.tmp_proj
-*.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
-
-# TFS 2012 Local Workspace
-$tf/
-
-# Guidance Automation Toolkit
-*.gpState
-
-# ReSharper is a .NET coding add-in
-_ReSharper*/
-*.[Rr]e[Ss]harper
-*.DotSettings.user
-
-# JustCode is a .NET coding add-in
-.JustCode
-
-# TeamCity is a build add-in
-_TeamCity*
-
-# DotCover is a Code Coverage Tool
-*.dotCover
-
-# 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
-# TODO: 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
-# The packages folder can be ignored because of Package Restore
-**/packages/*
-# except build/, which is used as an MSBuild target.
-!**/packages/build/
-# Uncomment if necessary however generally it will be regenerated when needed
-#!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable 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
-
-# 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
+# Dependencies
node_modules/
-orleans.codegen.cs
-
-# 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
-
-# SQL Server files
-*.mdf
-*.ldf
-# Business Intelligence projects
-*.rdl.data
-*.bim.layout
-*.bim_*.settings
+# Build output
+dist/
+dist-ssr/
+*.local
-# Microsoft Fakes
-FakesAssemblies/
+# Rust build output
+target/
-# GhostDoc plugin setting file
-*.GhostDoc.xml
-
-# Node.js Tools for Visual Studio
-.ntvs_analysis.dat
-
-# Visual Studio 6 build log
-*.plg
-
-# Visual Studio 6 workspace options file
-*.opt
-
-# 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/
-
-# JetBrains Rider
+# Logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
.idea/
-*.sln.iml
-
-# CodeRush
-.cr/
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
-/Dotfuscated
-/Build/Test
-/Build/Tools
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
-# LocaleRemulator runtime artifacts
-Beanfun/LRConfig.xml
-Beanfun/LRHookx32.dll
-Beanfun/LRHookx64.dll
-Beanfun/LRProc.exe
-Beanfun/LRSubMenus.dll
+# OS files
+Thumbs.db
# AI tool local settings
.claude/
# Local scripts
sync-upstream.ps1
+
+# Project-local task list (Cursor AI)
+Todo.md
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..53fc3d1
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,12 @@
+dist/
+node_modules/
+src-tauri/target/
+src-tauri/gen/
+mockups/
+package-lock.json
+*.lock
+
+# Auto-generated by `cargo run --example export_bindings` (tauri-specta).
+# File ships its own `/* prettier-ignore */` header but prettier's
+# CLI still flags it as unformatted, so ignore the whole path.
+src/types/bindings.ts
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..be737e8
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "endOfLine": "lf"
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..b9cd890
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["Vue.volar", "tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
+}
diff --git a/Beanfun.sln b/Beanfun.sln
deleted file mode 100644
index 856127a..0000000
--- a/Beanfun.sln
+++ /dev/null
@@ -1,37 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.1.32210.238
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beanfun", "Beanfun\Beanfun.csproj", "{4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|x64.ActiveCfg = Debug|x64
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|x64.Build.0 = Debug|x64
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|x86.ActiveCfg = Debug|Win32
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Debug|x86.Build.0 = Debug|Win32
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|Any CPU.Build.0 = Release|Any CPU
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|x64.ActiveCfg = Release|x64
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|x64.Build.0 = Release|x64
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|x86.ActiveCfg = Release|Win32
- {4B2F8D03-0C8F-4BAA-810D-DCACC37F5F87}.Release|x86.Build.0 = Release|Win32
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {D9F17966-B29B-4926-8EAD-44C6445E3565}
- EndGlobalSection
-EndGlobal
diff --git a/Beanfun/API/WCDESComp.cs b/Beanfun/API/WCDESComp.cs
deleted file mode 100644
index 448d569..0000000
--- a/Beanfun/API/WCDESComp.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Security.Cryptography;
-using System.Text;
-
-namespace Beanfun
-{
- class WCDESComp
- {
- public static string EncryStrHex(string str, string key)
- {
- try
- {
- using DES des = DES.Create();
- des.Mode = CipherMode.ECB;
- des.Padding = PaddingMode.None;
- des.Key = Encoding.ASCII.GetBytes(key);
- byte[] byteArray = Encoding.ASCII.GetBytes(str);
- ICryptoTransform desencrypt = des.CreateEncryptor();
- byte[] byteOUT = desencrypt.TransformFinalBlock(byteArray, 0, byteArray.Length);
- return BitConverter.ToString(byteOUT).Replace("-", "");
- }
- catch (Exception e)
- {
- Console.WriteLine("EncryptDESError:" + e.Message + "\n" + e.StackTrace);
- return null;
- }
- }
-
- public static string DecryStrHex(string hexString, string key)
- {
- try
- {
- using DES des = DES.Create();
- des.Mode = CipherMode.ECB;
- des.Padding = PaddingMode.None;
- des.Key = Encoding.ASCII.GetBytes(key);
- byte[] byteOUT = new byte[hexString.Length / 2];
- for (int i = 0; i < hexString.Length; i += 2)
- {
- byteOUT[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16);
- }
- ICryptoTransform desdecrypt = des.CreateDecryptor();
- return Encoding.ASCII.GetString(
- desdecrypt.TransformFinalBlock(byteOUT, 0, byteOUT.Length)
- );
- }
- catch (Exception e)
- {
- Console.WriteLine("DecryptDESError:" + e.Message + "\n" + e.StackTrace);
- return null;
- }
- }
- }
-}
diff --git a/Beanfun/API/WindowsAPI.cs b/Beanfun/API/WindowsAPI.cs
deleted file mode 100644
index cf0f950..0000000
--- a/Beanfun/API/WindowsAPI.cs
+++ /dev/null
@@ -1,255 +0,0 @@
-using System;
-using System.Drawing;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace Beanfun
-{
- static class WindowsAPI
- {
- [DllImport("user32.dll", SetLastError = true)]
- public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
-
- [DllImport("user32.dll", SetLastError = true)]
- public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
-
- [DllImport("user32.dll")]
- public static extern bool SetForegroundWindow(IntPtr hWnd);
-
- [DllImport("user32.dll")]
- public static extern byte MapVirtualKey(byte wCode, int wMap);
-
- public static void PostString(IntPtr hwnd, string input)
- {
- const int WM_CHAR = 0x102;
- byte[] chars = ASCIIEncoding.ASCII.GetBytes(input);
- foreach (byte ch in chars)
- {
- PostMessage(hwnd, WM_CHAR, ch, 0);
- }
- }
-
- public static void PostKey(IntPtr hWnd, uint wMsg, byte wParam)
- {
- PostMessage(hWnd, wMsg, wParam, MapVirtualKey(wParam, 0) << 16 + 1);
- }
-
- [DllImport("user32.dll")]
- public static extern int PostMessage(IntPtr hWnd, uint wMsg, int wParam, int lParam);
-
- [DllImport("user32.dll")]
- public static extern bool ClientToScreen(IntPtr hWnd, ref System.Drawing.Point lpPoint);
-
- [DllImport("user32.dll")]
- public static extern bool GetCursorPos(ref System.Drawing.Point lpPoint);
-
- [DllImport("user32.dll")]
- public static extern int SetCursorPos(int x, int y);
-
- [DllImport("user32.dll", SetLastError = true)]
- public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
-
- [DllImport("user32.dll")]
- public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
-
- [DllImport("user32.dll")]
- public static extern int SetWindowCompositionAttribute(
- IntPtr hwnd,
- ref WindowCompositionAttributeData data
- );
-
- [StructLayout(LayoutKind.Sequential)]
- private struct RECT
- {
- public int Left;
- public int Top;
- public int Right;
- public int Bottom;
- }
-
- [DllImport("user32.dll")]
- private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
-
- public static Size GetClientAreaSize(IntPtr hWnd)
- {
- if (hWnd == IntPtr.Zero)
- throw new ArgumentException();
-
- if (GetClientRect(hWnd, out RECT clientRect))
- {
- int width = clientRect.Right - clientRect.Left;
- int height = clientRect.Bottom - clientRect.Top;
- return new Size(width, height);
- }
-
- return Size.Empty;
- }
-
- public enum AccentState
- {
- ACCENT_DISABLED = 0,
-
- ACCENT_ENABLE_GRADIENT = 1,
- ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
- ACCENT_ENABLE_BLURBEHIND = 3,
- ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
- ACCENT_INVALID_STATE = 5,
- }
-
- [StructLayout(LayoutKind.Sequential)]
- public struct AccentPolicy
- {
- public AccentState AccentState;
- public int AccentFlags;
- public int GradientColor;
- public int AnimationId;
- }
-
- [StructLayout(LayoutKind.Sequential)]
- public struct WindowCompositionAttributeData
- {
- public WindowCompositionAttribute Attribute;
- public IntPtr Data;
- public int SizeOfData;
- }
-
- public enum WindowCompositionAttribute
- {
- WCA_ACCENT_POLICY = 19,
- }
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- private static extern Int32 GetSystemDefaultLocaleName(
- [Out] StringBuilder lpLocaleName,
- Int32 cchLocaleName
- );
-
- public const Int32 LOCALE_NAME_MAX_LENGTH = 85;
-
- public static String GetSystemDefaultLocaleName()
- {
- StringBuilder lpLocaleName = new StringBuilder(LOCALE_NAME_MAX_LENGTH);
- if (GetSystemDefaultLocaleName(lpLocaleName, LOCALE_NAME_MAX_LENGTH) > 0)
- {
- return lpLocaleName.ToString();
- }
-
- return null;
- }
-
- [DllImport("kernel32.dll")]
- public static extern IntPtr GetCurrentProcess();
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- public static extern IntPtr GetModuleHandle(string moduleName);
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- public static extern IntPtr GetProcAddress(
- IntPtr hModule,
- [MarshalAs(UnmanagedType.LPStr)] string procName
- );
-
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool IsWow64Process(IntPtr hProcess, out bool wow64Process);
-
- [DllImport("kernel32.dll")]
- public static extern bool AttachConsole(int processId);
-
- public enum BinaryType : uint
- {
- SCS_32BIT_BINARY = 0, // A 32-bit Windows-based application
- SCS_64BIT_BINARY = 6, // A 64-bit Windows-based application.
- SCS_DOS_BINARY = 1, // An MS-DOS � based application
- SCS_OS216_BINARY = 5, // A 16-bit OS/2-based application
- SCS_PIF_BINARY = 3, // A PIF file that executes an MS-DOS � based application
- SCS_POSIX_BINARY = 4, // A POSIX � based application
- SCS_WOW_BINARY = 2, // A 16-bit Windows-based application
- }
-
- [DllImport("kernel32.dll")]
- public static extern bool GetBinaryType(
- string lpApplicationName,
- out BinaryType lpBinaryType
- );
-
- public enum dwMapFlags : uint
- {
- NORM_IGNORECASE = 0x00000001,
- NORM_IGNORENONSPACE = 0x00000002,
- NORM_IGNORESYMBOLS = 0x00000004,
- LCMAP_LOWERCASE = 0x00000100,
- LCMAP_UPPERCASE = 0x00000200,
- LCMAP_SORTKEY = 0x00000400,
- LCMAP_BYTEREV = 0x00000800,
- SORT_STRINGSORT = 0x00001000,
- NORM_IGNOREKANATYPE = 0x00010000,
- NORM_IGNOREWIDTH = 0x00020000,
- LCMAP_HIRAGANA = 0x00100000,
- LCMAP_KATAKANA = 0x00200000,
- LCMAP_HALFWIDTH = 0x00400000,
- LCMAP_FULLWIDTH = 0x00800000,
- LCMAP_LINGUISTIC_CASING = 0x01000000,
- LCMAP_SIMPLIFIED_CHINESE = 0x02000000,
- LCMAP_TRADITIONAL_CHINESE = 0x04000000,
- }
-
- [DllImport("kernel32.dll")]
- public static extern int LCMapStringW(
- int Locale,
- uint dwMapFlags,
- [MarshalAs(UnmanagedType.LPWStr)] string lpSrcStr,
- int cchSrc,
- [MarshalAs(UnmanagedType.LPWStr)] string lpDestStr,
- int cchDest
- );
-
- [DllImport("user32.dll")]
- private static extern bool OpenClipboard(IntPtr hWndNewOwner);
-
- [DllImport("user32.dll")]
- private static extern bool CloseClipboard();
-
- [DllImport("user32.dll")]
- private static extern bool EmptyClipboard();
-
- [DllImport("user32.dll")]
- private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
-
- [DllImport("kernel32.dll")]
- private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
-
- [DllImport("kernel32.dll")]
- private static extern IntPtr GlobalLock(IntPtr hMem);
-
- [DllImport("kernel32.dll")]
- private static extern bool GlobalUnlock(IntPtr hMem);
-
- private const uint CF_UNICODETEXT = 13;
- private const uint GMEM_MOVEABLE = 0x0002;
-
- public static bool CopyText(string text)
- {
- if (!OpenClipboard(IntPtr.Zero))
- return false;
- try
- {
- EmptyClipboard();
- int bytes = (text.Length + 1) * 2;
- IntPtr hGlobal = GlobalAlloc(GMEM_MOVEABLE, (UIntPtr)bytes);
- if (hGlobal == IntPtr.Zero)
- return false;
- IntPtr target = GlobalLock(hGlobal);
- Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
- Marshal.WriteInt16(target, text.Length * 2, 0);
- GlobalUnlock(hGlobal);
- SetClipboardData(CF_UNICODETEXT, hGlobal);
- return true;
- }
- finally
- {
- CloseClipboard();
- }
- }
- }
-}
diff --git a/Beanfun/App.xaml b/Beanfun/App.xaml
deleted file mode 100644
index 313acf1..0000000
--- a/Beanfun/App.xaml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- Resources/#Segoe MDL2 Assets
-
-
-
-
-
-
-
diff --git a/Beanfun/App.xaml.cs b/Beanfun/App.xaml.cs
deleted file mode 100644
index 03b9369..0000000
--- a/Beanfun/App.xaml.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Reflection;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Windows;
-using System.Windows.Interop;
-using System.Windows.Media;
-
-namespace Beanfun
-{
- ///
- /// App.xaml 的交互逻辑
- ///
- public partial class App : Application
- {
- public static readonly Version OSVersion = Environment.OSVersion.Version;
- public static readonly Version Win2000 = new Version(5, 0);
- public static readonly Version WinXP = new Version(5, 1);
- public static readonly Version Win2003 = new Version(5, 2);
- public static readonly Version WinVista = new Version(6, 0);
- public static readonly Version Win7 = new Version(6, 1);
- public static readonly Version Win8 = new Version(6, 2);
- public static readonly Version Win8_1 = new Version(6, 3);
- public static readonly Version Win10 = new Version(10, 0);
- public static readonly Version Win11 = new Version(10, 0, 22000, 0);
-
- public static MainWindow MainWnd
- {
- get
- {
- Window wnd = Current.MainWindow;
- if (wnd != null && (typeof(MainWindow) == wnd.GetType()))
- return (MainWindow)wnd;
- else
- return null;
- }
- }
-
- public static string LoginRegion = ConfigAppSettings.GetValue("loginRegion", "TW");
- public static int LoginMethod = int.Parse(ConfigAppSettings.GetValue("loginMethod", "0"));
-
- private void Main(object sender, StartupEventArgs e)
- {
- WindowsAPI.AttachConsole(-1);
-
- if (bool.Parse(ConfigAppSettings.GetValue("disableHardwareAcceleration", "false")))
- RenderOptions.ProcessRenderMode = RenderMode.SoftwareOnly;
-
- I18n.LoadLanguage(ConfigAppSettings.GetValue("Language", null));
-
- StartupUri = new Uri("MainWindow.xaml", UriKind.RelativeOrAbsolute);
- }
-
- public bool compareFile(string path1, string path2)
- {
- using var hash = MD5.Create();
- using var stream_1 = File.OpenRead(path1);
- byte[] hashByte_1 = hash.ComputeHash(stream_1);
-
- using var stream_2 = File.OpenRead(path2);
- byte[] hashByte_2 = hash.ComputeHash(stream_2);
-
- return BitConverter.ToString(hashByte_1) == BitConverter.ToString(hashByte_2);
- }
-
- private void Application_Exit(object sender, ExitEventArgs e)
- {
- if (MainWnd != null && MainWnd.bfClient != null)
- try
- {
- MainWnd.bfClient.Logout();
- }
- catch { }
- }
-
- // --- 版本轉換邏輯 (處理幽靈點問題) ---
- public static string ConvertVersion(Version version)
- {
- if (version < new Version(4, 1))
- return $"{version.Major}.{version.Minor}.{version.Build}({version.Revision})";
-
- DateTime buildDate = new DateTime(2000, 1, 1)
- .AddDays(version.Build)
- .AddSeconds(version.Revision * 2);
-
- string timestamp = buildDate.ToString("yyMMddHHmm");
-
- // 關鍵:如果 Build < 1000 代表係 Patch 號碼
- if (version.Build < 1000)
- {
- // 格式: 5.8.3(2604011114)
- return $"{version.Major}.{version.Minor}.{version.Build}({timestamp})";
- }
- else
- {
- // 格式: 5.8(2604011114)
- return $"{version.Major}.{version.Minor}({timestamp})";
- }
- }
-
- internal static string AssemblyVersion
- {
- get
- {
- var attr = Assembly
- .GetExecutingAssembly()
- .GetCustomAttribute();
- if (attr != null && !string.IsNullOrEmpty(attr.InformationalVersion))
- {
- string ver = attr.InformationalVersion;
-
- int plusIndex = ver.IndexOf('+');
- if (plusIndex > 0)
- ver = ver.Substring(0, plusIndex);
-
- if (ver.Contains("("))
- return ver;
- }
-
- return ConvertVersion(Assembly.GetExecutingAssembly().GetName().Version);
- }
- }
-
- public static readonly string AppDir = Path.GetDirectoryName(
- Process.GetCurrentProcess().MainModule.FileName
- );
-
- public static int ReleaseResource(string file)
- {
- string path = Path.Combine(AppDir, file);
- using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(file))
- {
- if (stream != null)
- {
- if (File.Exists(path))
- {
- var fileInfo = new FileInfo(path);
- if (fileInfo.Length == stream.Length)
- return 0;
-
- try
- {
- File.Delete(path);
- }
- catch
- {
- return -1;
- }
- }
-
- string dir = Path.GetDirectoryName(path);
- if (!Directory.Exists(dir))
- Directory.CreateDirectory(dir);
-
- stream.Position = 0;
- File.WriteAllBytes(
- path,
- new BinaryReader(stream).ReadBytes((int)stream.Length)
- );
- return 1;
- }
- }
- return -1;
- }
-
- public static string GetMD5HashFromFile(string fileName)
- {
- try
- {
- using FileStream file = new FileStream(
- fileName,
- FileMode.Open,
- FileAccess.Read,
- FileShare.ReadWrite
- );
- return GetMD5HashFromStream(file);
- }
- catch (Exception ex)
- {
- throw new Exception("GetMD5HashFromFile() fail, error: " + ex.Message);
- }
- }
-
- public static string GetMD5HashFromStream(Stream stream)
- {
- try
- {
- using MD5 md5 = MD5.Create();
- byte[] retVal = md5.ComputeHash(stream);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < retVal.Length; i++)
- {
- sb.Append(retVal[i].ToString("x2"));
- }
- return sb.ToString();
- }
- catch (Exception ex)
- {
- throw new Exception("GetMD5HashFromStream() fail, error: " + ex.Message);
- }
- }
- }
-}
diff --git a/Beanfun/Beanfun.csproj b/Beanfun/Beanfun.csproj
deleted file mode 100644
index dc7918a..0000000
--- a/Beanfun/Beanfun.csproj
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
- WinExe
- net8.0-windows
- true
- true
- Beanfun
- Beanfun
- Resources\icon.ico
- Properties\app.manifest
- Beanfun.App
- true
- false
- false
-
- $(NoWarn);CA1416;SYSLIB0014
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
- True
- True
- Resources.resx
-
-
-
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
- True
- Settings.settings
- True
-
-
-
-
-
-
- LRConfig.xml
-
-
- LRHookx32.dll
-
-
- LRHookx64.dll
-
-
- LRProc.exe
-
-
- LRSubMenus.dll
-
-
-
diff --git a/Beanfun/Helper/AccountManager.cs b/Beanfun/Helper/AccountManager.cs
deleted file mode 100644
index d609cbb..0000000
--- a/Beanfun/Helper/AccountManager.cs
+++ /dev/null
@@ -1,554 +0,0 @@
-/*
- * 開發此功能主要用為多帳號時儲存
- * 以原有加解密寫法為基礎
- * 加上一層wrapper並用Serializable方式儲存資料
- * thanks to Stackoverflow :p
- * http://stackoverflow.com/questions/5869922/c-sharp-encrypt-serialized-file-before-writing-to-disk
- * http://stackoverflow.com/questions/16352879/write-list-of-objects-to-a-file
- *
- * Date: 2016/3/1
- * Author: 葉家郡 (a.k.a 某數)
- */
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using Newtonsoft.Json;
-using Utility.ModifyRegistry;
-
-namespace Beanfun
-{
- [Serializable]
- class AccountRecords
- {
- public List regionList = null,
- accountList = null,
- passwdList = null,
- verifyList = null;
- public List methodList = null;
- public List autoLoginList = null;
- }
-
- [Serializable]
- class Records
- {
- public List regionList = null,
- accountList = null,
- accountNameList = null,
- passwdList = null,
- verifyList = null;
- public List methodList = null;
- public List autoLoginList = null;
-
- public static Records Change(object oldRecords)
- {
- Records res = new Records();
- if (oldRecords is AccountRecords)
- {
- AccountRecords records = (AccountRecords)oldRecords;
- res.regionList = records.regionList;
- res.accountList = records.accountList;
- res.passwdList = records.passwdList;
- res.verifyList = records.verifyList;
- res.methodList = records.methodList;
- res.autoLoginList = records.autoLoginList;
- }
- return res;
- }
- }
-
- public class AccountManager
- {
- private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
- typeof(AccountManager)
- );
-
- private Records accountRecords = null;
- private string dataPath =
- System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)
- + "\\Beanfun\\Users.dat";
-
- public bool init()
- {
- return loadRecord();
- }
-
- #region helper function
- private void accRecInit()
- {
- if (accountRecords == null)
- accountRecords = new Records();
-
- if (accountRecords.accountList == null)
- accountRecords.accountList = new List();
-
- if (accountRecords.regionList == null)
- accountRecords.regionList = new List();
- if (accountRecords.regionList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.regionList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.regionList.Add("TW");
- }
- }
-
- if (accountRecords.accountNameList == null)
- accountRecords.accountNameList = new List();
- if (accountRecords.accountNameList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.accountNameList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.accountNameList.Add("");
- }
- }
-
- if (accountRecords.passwdList == null)
- accountRecords.passwdList = new List();
- if (accountRecords.passwdList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.passwdList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.passwdList.Add("");
- }
- }
-
- if (accountRecords.verifyList == null)
- accountRecords.verifyList = new List();
- if (accountRecords.verifyList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.verifyList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.verifyList.Add("");
- }
- }
-
- if (accountRecords.methodList == null)
- accountRecords.methodList = new List();
- if (accountRecords.methodList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.methodList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.methodList.Add(0);
- }
- }
-
- if (accountRecords.autoLoginList == null)
- accountRecords.autoLoginList = new List();
- if (accountRecords.autoLoginList.Count < accountRecords.accountList.Count)
- {
- for (
- int i = accountRecords.autoLoginList.Count;
- i < accountRecords.accountList.Count;
- i++
- )
- {
- accountRecords.autoLoginList.Add(false);
- }
- }
- }
-
- private bool loadRecord()
- {
- var raw = readRawData();
- if (raw != null)
- {
- try
- {
- // 嘗試以新版 JSON 格式讀取資料
- accountRecords = JsonConvert.DeserializeObject(raw);
- }
- catch
- {
- accountRecords = null;
- // 解析失敗時,自動視為舊版 BinaryFormatter 格式並嘗試進行無縫轉換
- TryAutoMigrateLegacyData(raw);
- }
- }
- accRecInit();
-
- return true;
- }
-
- private bool storeRecord()
- {
- string json = JsonConvert.SerializeObject(accountRecords);
- writeRawData(json);
- return true;
- }
- #endregion
-
- #region rawdata IO
- /*
- * read ciphertext from File
- * decrypt it and return
- */
- private string readRawData()
- {
- try
- {
- if (File.Exists(dataPath))
- {
- try
- {
- Byte[] cipher = File.ReadAllBytes(dataPath);
- ModifyRegistry myRegistry = new ModifyRegistry();
- myRegistry.BaseRegistryKey = Microsoft.Win32.Registry.CurrentUser;
- string entropy = myRegistry.Read("Entropy");
- byte[] plaintext = ProtectedData.Unprotect(
- cipher,
- Encoding.UTF8.GetBytes(entropy),
- DataProtectionScope.CurrentUser
- );
- return Encoding.UTF8.GetString(plaintext);
- }
- catch
- {
- File.Delete(dataPath);
- }
- }
-
- return null;
- }
- catch
- {
- return null;
- }
- }
-
- /*
- * encrypt plaintext and store to File
- * and save key in Program Setting
- */
- private void writeRawData(string plaintext)
- {
- using (BinaryWriter writer = new BinaryWriter(File.Open(dataPath, FileMode.Create)))
- {
- var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- var random = new Random();
- string entropy = new string(
- Enumerable.Repeat(chars, 8).Select(s => s[random.Next(s.Length)]).ToArray()
- );
-
- ModifyRegistry myRegistry = new ModifyRegistry();
- myRegistry.BaseRegistryKey = Microsoft.Win32.Registry.CurrentUser;
- myRegistry.Write("Entropy", entropy);
-
- writer.Write(ciphertext(plaintext, entropy));
- }
- }
-
- private byte[] ciphertext(string plaintext, string key)
- {
- byte[] plainByte = Encoding.UTF8.GetBytes(plaintext);
- byte[] entropy = Encoding.UTF8.GetBytes(key);
- return ProtectedData.Protect(plainByte, entropy, DataProtectionScope.CurrentUser);
- }
- #endregion
-
- #region Interface
- public bool addAccount(
- string region,
- string account,
- string name,
- string password,
- string verify,
- int method,
- bool autoLogin
- )
- {
- return addAccount(-1, region, account, name, password, verify, method, autoLogin);
- }
-
- public bool addAccount(
- int index,
- string region,
- string account,
- string name,
- string password,
- string verify,
- int method,
- bool autoLogin
- )
- {
- bool isExists = false;
- List regionIndex = new List();
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (region != accountRecords.regionList[i])
- {
- continue;
- }
- if (account == accountRecords.accountList[i])
- {
- if (index > -1 && regionIndex.Count != index)
- {
- removeAccount(region, account);
- i--;
- continue;
- }
- accountRecords.accountNameList[i] = name;
- accountRecords.passwdList[i] = password;
- accountRecords.verifyList[i] = verify;
- accountRecords.methodList[i] = method;
- accountRecords.autoLoginList[i] = autoLogin;
- isExists = true;
- break;
- }
- regionIndex.Add(i);
- }
-
- if (!isExists)
- {
- if (index < 0 || regionIndex.Count <= index)
- {
- accountRecords.regionList.Add(region);
- accountRecords.accountList.Add(account);
- accountRecords.accountNameList.Add(name);
- accountRecords.passwdList.Add(password);
- accountRecords.verifyList.Add(verify);
- accountRecords.methodList.Add(method);
- accountRecords.autoLoginList.Add(autoLogin);
- }
- else
- {
- index = regionIndex[index];
- accountRecords.regionList.Insert(index, region);
- accountRecords.accountList.Insert(index, account);
- accountRecords.accountNameList.Insert(index, name);
- accountRecords.passwdList.Insert(index, password);
- accountRecords.verifyList.Insert(index, verify);
- accountRecords.methodList.Insert(index, method);
- accountRecords.autoLoginList.Insert(index, autoLogin);
- }
- }
-
- storeRecord();
-
- return true;
- }
-
- public string getNameByAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- return accountRecords.accountNameList[i];
- }
- }
- return null;
- }
-
- public string getPasswordByAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- return accountRecords.passwdList[i];
- }
- }
- return null;
- }
-
- public string getVerifyByAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- return accountRecords.verifyList[i];
- }
- }
- return null;
- }
-
- public int getMethodByAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- return accountRecords.methodList[i];
- }
- }
- return -1;
- }
-
- public bool getAutoLoginByAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- return accountRecords.autoLoginList[i];
- }
- }
- return false;
- }
-
- public bool removeAccount(string region, string account)
- {
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (
- account == accountRecords.accountList[i]
- && region == accountRecords.regionList[i]
- )
- {
- accountRecords.regionList.RemoveAt(i);
- accountRecords.accountList.RemoveAt(i);
- accountRecords.accountNameList.RemoveAt(i);
- accountRecords.passwdList.RemoveAt(i);
- accountRecords.verifyList.RemoveAt(i);
- accountRecords.methodList.RemoveAt(i);
- accountRecords.autoLoginList.RemoveAt(i);
-
- storeRecord();
- return true;
- }
- }
- return false;
- }
-
- public string[] getAccountList()
- {
- return accountRecords.accountList.ToArray();
- }
-
- public string[] getAccountList(string region)
- {
- List accList = new List();
- for (int i = 0; i < accountRecords.accountList.Count; ++i)
- {
- if (region == accountRecords.regionList[i])
- {
- accList.Add(accountRecords.accountList[i]);
- }
- }
- return accList.ToArray();
- }
-
- public bool importRecord(string raw)
- {
- try
- {
- accountRecords = JsonConvert.DeserializeObject(raw);
- accRecInit();
- storeRecord();
- return true;
- }
- catch
- {
- // 匯入失敗時,嘗試將其視為舊版格式進行轉換
- return TryAutoMigrateLegacyData(raw);
- }
- }
-
- public string exportRecord()
- {
- return JsonConvert.SerializeObject(accountRecords);
- }
- #endregion
-
- #region Legacy format migration
- // Fix #182: 實作內建的舊版資料自動升級機制,取代原先會導致 404 的外部轉換工具
- // TODO: 此升級機制僅為過渡用途。建議於發布幾個版本後,確認多數活躍玩家皆已轉換至 JSON 格式時,將此方法徹底移除。
- private bool TryAutoMigrateLegacyData(string raw)
- {
- try
- {
- byte[] cipher;
- try
- {
- cipher = Convert.FromBase64String(raw);
- }
- catch (FormatException)
- {
- return false;
- }
- using (var stream = new MemoryStream(cipher))
- {
- // 忽略編譯器針對 BinaryFormatter 的安全性警告
- // 注意:此類別極度不安全,僅限於此處讀取舊版資料使用,新代碼嚴禁使用!
-#pragma warning disable SYSLIB0011
- var bformatter =
- new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
- object oldRecords = bformatter.Deserialize(stream);
-#pragma warning restore SYSLIB0011
-
- if (oldRecords != null)
- {
- // 透過 JSON 序列化作為中介,避免類別轉型 (Casting) 發生例外狀況
- string tempJson = JsonConvert.SerializeObject(oldRecords);
- accountRecords = JsonConvert.DeserializeObject(tempJson);
-
- if (accountRecords != null)
- {
- accRecInit();
- storeRecord(); // 立即將轉換後的資料以最新 JSON 格式寫入,覆寫舊檔
-
- log.Info("Legacy account data auto-migrated to JSON format.");
- System.Windows.MessageBox.Show(
- System.Windows.Application.Current.TryFindResource(
- "LegacyDataMigrateSuccess"
- ) as string,
- System.Windows.Application.Current.TryFindResource(
- "LegacyDataMigrateTitle"
- ) as string,
- System.Windows.MessageBoxButton.OK,
- System.Windows.MessageBoxImage.Information
- );
- return true;
- }
- }
- }
- }
- catch (Exception ex)
- {
- // 若因 .NET 版本限制或資料損毀導致轉換失敗,則記錄錯誤,並讓 accRecInit 建立新的空白紀錄
- log.Error($"Auto-migration of legacy data failed: {ex.Message}");
- }
-
- return false;
- }
- #endregion
- }
-}
diff --git a/Beanfun/Helper/ConfigAppSettings.cs b/Beanfun/Helper/ConfigAppSettings.cs
deleted file mode 100644
index 1566415..0000000
--- a/Beanfun/Helper/ConfigAppSettings.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using System.Configuration;
-using System.IO;
-
-namespace Beanfun
-{
- class ConfigAppSettings
- {
- public static void SetValue(string key, string value)
- {
- try
- {
- ExeConfigurationFileMap map = new ExeConfigurationFileMap();
- map.ExeConfigFilename =
- System.Environment.GetFolderPath(
- System.Environment.SpecialFolder.ApplicationData
- ) + "\\Beanfun\\Config.xml";
- Configuration config = ConfigurationManager.OpenMappedExeConfiguration(
- map,
- ConfigurationUserLevel.None
- );
- if (config.AppSettings.Settings[key] == null)
- {
- if (value != null)
- config.AppSettings.Settings.Add(key, value);
- }
- else
- {
- if (value == null)
- config.AppSettings.Settings.Remove(key);
- else
- config.AppSettings.Settings[key].Value = value;
- }
- config.Save(ConfigurationSaveMode.Modified);
- ConfigurationManager.RefreshSection("appSettings");
- }
- catch
- {
- try
- {
- string filePath =
- System.Environment.GetFolderPath(
- System.Environment.SpecialFolder.ApplicationData
- ) + "\\Beanfun";
- DirectoryInfo dir = new DirectoryInfo(filePath);
- FileSystemInfo[] fileinfo = dir.GetFileSystemInfos("Config.xml");
- foreach (FileSystemInfo i in fileinfo)
- {
- if (i is DirectoryInfo)
- {
- DirectoryInfo subdir = new DirectoryInfo(i.FullName);
- subdir.Delete(true);
- }
- else
- {
- File.Delete(i.FullName);
- }
- }
- SetValue(key, value);
- }
- catch { }
- }
- }
-
- public static string GetValue(string key)
- {
- return GetValue(key, string.Empty);
- }
-
- public static string GetValue(string key, string def)
- {
- string value;
- try
- {
- ExeConfigurationFileMap map = new ExeConfigurationFileMap();
- map.ExeConfigFilename =
- System.Environment.GetFolderPath(
- System.Environment.SpecialFolder.ApplicationData
- ) + "\\Beanfun\\Config.xml";
- Configuration config = ConfigurationManager.OpenMappedExeConfiguration(
- map,
- ConfigurationUserLevel.None
- );
- value =
- config.AppSettings.Settings[key] == null
- ? def
- : config.AppSettings.Settings[key].Value;
- }
- catch
- {
- value = def;
- }
- return value;
- }
- }
-}
diff --git a/Beanfun/Helper/I18n.cs b/Beanfun/Helper/I18n.cs
deleted file mode 100644
index e098a1f..0000000
--- a/Beanfun/Helper/I18n.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Reflection;
-using System.Windows;
-using System.Windows.Markup;
-
-namespace Beanfun
-{
- class I18n
- {
- public static string CultureName { get; set; }
- public static List CultureArray { get; } = new List();
-
- internal static void LoadLanguage(string lang = null)
- {
- CultureInfo currentCultureInfo;
- if (lang == null)
- currentCultureInfo = CultureInfo.CurrentUICulture;
- else
- {
- currentCultureInfo = CultureInfo.GetCultureInfo(lang);
- if (currentCultureInfo == null)
- currentCultureInfo = CultureInfo.CurrentUICulture;
- }
-
- CultureName = currentCultureInfo.Name;
- CultureArray.Clear();
- while (true)
- {
- if (string.IsNullOrEmpty(currentCultureInfo.Name))
- break;
- CultureArray.Insert(0, currentCultureInfo.Name);
- currentCultureInfo = currentCultureInfo.Parent;
- }
-
- ResourceDictionary defaultDict = null;
- if (Application.Current.Resources.MergedDictionaries.Count > 0)
- defaultDict = Application.Current.Resources.MergedDictionaries[0];
- Application.Current.Resources.MergedDictionaries.Clear();
- if (defaultDict != null)
- Application.Current.Resources.MergedDictionaries.Add(defaultDict);
-
- try
- {
- var langDir = Path.Combine(AppContext.BaseDirectory, @"Lang\");
-
- ResourceDictionary dictionary = null;
- string langPath = null;
- string langUri = null;
- foreach (string cultureName in CultureArray)
- {
- langPath = Path.Combine(langDir, cultureName + ".xaml");
- if (File.Exists(langPath))
- dictionary =
- XamlReader.Load(new FileStream(langPath, FileMode.Open))
- as ResourceDictionary;
- else if (!cultureName.ToUpper().Equals("ZH"))
- {
- langUri = $@"/Beanfun;Component/Lang/{cultureName}.xaml";
- try
- {
- dictionary = new ResourceDictionary
- {
- Source = new Uri(langUri, UriKind.Relative),
- };
- }
- catch { }
- }
-
- if (dictionary != null)
- Application.Current.Resources.MergedDictionaries.Add(dictionary);
- }
- }
- catch { }
-
- if (Application.Current.Resources.MergedDictionaries.Count <= 0)
- throw new Exception("No language file.");
- }
-
- internal static string ToSimplified(string argSource)
- {
- if (!CultureArray.Contains("zh-Hans"))
- return argSource;
- var t = new String(' ', argSource.Length);
- WindowsAPI.LCMapStringW(
- CultureInfo.CurrentUICulture.LCID,
- (int)WindowsAPI.dwMapFlags.LCMAP_SIMPLIFIED_CHINESE,
- argSource,
- argSource.Length,
- t,
- argSource.Length
- );
- return t;
- }
- }
-}
diff --git a/Beanfun/Helper/ModifyRegistry.cs b/Beanfun/Helper/ModifyRegistry.cs
deleted file mode 100644
index b92df90..0000000
--- a/Beanfun/Helper/ModifyRegistry.cs
+++ /dev/null
@@ -1,278 +0,0 @@
-/* ***************************************
- * ModifyRegistry.cs
- * ---------------------------------------
- * a very simple class
- * to read, write, delete and count
- * registry values with C#
- * ---------------------------------------
- * if you improve this code
- * please email me your improvement!
- * ---------------------------------------
- * by Francesco Natali
- * - fn.varie@libero.it -
- * ***************************************/
-
-using System;
-// and for the MessageBox function:
-using System.Windows;
-// it's required for reading/writing into the registry:
-using Microsoft.Win32;
-
-namespace Utility.ModifyRegistry
-{
- ///
- /// An useful class to read/write/delete/count registry keys
- ///
- class ModifyRegistry
- {
- private bool showError = false;
-
- ///
- /// A property to show or hide error messages
- /// (default = false)
- ///
- public bool ShowError
- {
- get { return showError; }
- set { showError = value; }
- }
-
- private string subKey =
- "SOFTWARE\\" + Application.ResourceAssembly.GetName().Name.ToUpper();
-
- ///
- /// A property to set the SubKey value
- /// (default = "SOFTWARE\\" + Application.ProductName.ToUpper())
- ///
- public string SubKey
- {
- get { return subKey; }
- set { subKey = value; }
- }
-
- private RegistryKey baseRegistryKey = Registry.LocalMachine;
-
- ///
- /// A property to set the BaseRegistryKey value.
- /// (default = Registry.LocalMachine)
- ///
- public RegistryKey BaseRegistryKey
- {
- get { return baseRegistryKey; }
- set { baseRegistryKey = value; }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// To read a registry key.
- /// input: KeyName (string)
- /// output: value (string)
- ///
- public string Read(string KeyName)
- {
- // Opening the registry key
- RegistryKey rk = baseRegistryKey;
- // Open a subKey as read-only
- RegistryKey sk1 = rk.OpenSubKey(subKey);
- // If the RegistrySubKey doesn't exist -> (null)
- if (sk1 == null)
- {
- return null;
- }
- else
- {
- try
- {
- // If the RegistryKey exists I get its value
- // or null is returned.
- return (string)sk1.GetValue(KeyName.ToUpper());
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Reading registry " + KeyName.ToUpper());
- return null;
- }
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// To write into a registry key.
- /// input: KeyName (string) , Value (object)
- /// output: true or false
- ///
- public bool Write(string KeyName, object Value)
- {
- try
- {
- // Setting
- RegistryKey rk = baseRegistryKey;
- // I have to use CreateSubKey
- // (create or open it if already exits),
- // 'cause OpenSubKey open a subKey as read-only
- RegistryKey sk1 = rk.CreateSubKey(subKey);
- // Save the value
- sk1.SetValue(KeyName.ToUpper(), Value);
-
- return true;
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Writing registry " + KeyName.ToUpper());
- return false;
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// To delete a registry key.
- /// input: KeyName (string)
- /// output: true or false
- ///
- public bool DeleteKey(string KeyName)
- {
- try
- {
- // Setting
- RegistryKey rk = baseRegistryKey;
- RegistryKey sk1 = rk.CreateSubKey(subKey);
- // If the RegistrySubKey doesn't exists -> (true)
- if (sk1 == null)
- return true;
- else
- sk1.DeleteValue(KeyName);
-
- return true;
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Deleting SubKey " + subKey);
- return false;
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// To create a sub key.
- ///
- public void CreateSubKey()
- {
- // Opening the registry key
- RegistryKey rk = baseRegistryKey;
- // Open a subKey as read-only
- RegistryKey sk1 = rk.OpenSubKey(subKey);
- // If the RegistrySubKey doesn't exist -> (null)
- if (sk1 == null)
- {
- rk.CreateSubKey(subKey);
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// To delete a sub key and any child.
- /// input: void
- /// output: true or false
- ///
- public bool DeleteSubKeyTree()
- {
- try
- {
- // Setting
- RegistryKey rk = baseRegistryKey;
- RegistryKey sk1 = rk.OpenSubKey(subKey);
- // If the RegistryKey exists, I delete it
- if (sk1 != null)
- rk.DeleteSubKeyTree(subKey);
-
- return true;
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Deleting SubKey " + subKey);
- return false;
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// Retrive the count of subkeys at the current key.
- /// input: void
- /// output: number of subkeys
- ///
- public int SubKeyCount()
- {
- try
- {
- // Setting
- RegistryKey rk = baseRegistryKey;
- RegistryKey sk1 = rk.OpenSubKey(subKey);
- // If the RegistryKey exists...
- if (sk1 != null)
- return sk1.SubKeyCount;
- else
- return 0;
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Retriving subkeys of " + subKey);
- return 0;
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- ///
- /// Retrive the count of values in the key.
- /// input: void
- /// output: number of keys
- ///
- public int ValueCount()
- {
- try
- {
- // Setting
- RegistryKey rk = baseRegistryKey;
- RegistryKey sk1 = rk.OpenSubKey(subKey);
- // If the RegistryKey exists...
- if (sk1 != null)
- return sk1.ValueCount;
- else
- return 0;
- }
- catch (Exception e)
- {
- // AAAAAAAAAAARGH, an error!
- ShowErrorMessage(e, "Retriving keys of " + subKey);
- return 0;
- }
- }
-
- /* **************************************************************************
- * **************************************************************************/
-
- private void ShowErrorMessage(Exception e, string Title)
- {
- if (showError == true)
- MessageBox.Show(e.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-}
diff --git a/Beanfun/Helper/PacketReader.cs b/Beanfun/Helper/PacketReader.cs
deleted file mode 100644
index 4892c02..0000000
--- a/Beanfun/Helper/PacketReader.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-
-namespace MapleLib.PacketLib
-{
- ///
- /// Class to handle reading data from a packet
- ///
- public class PacketReader : IDisposable
- {
- protected MemoryStream _buffer;
-
- ///
- /// The main reader tool
- ///
- private readonly BinaryReader _binReader;
-
- ///
- /// Amount of data left in the reader
- ///
- public int Length
- {
- get { return (int)_buffer.Length; }
- }
-
- ///
- /// Creates a new instance of PacketReader
- ///
- /// Starting byte array
- public PacketReader(byte[] arrayOfBytes)
- {
- _buffer = new MemoryStream(arrayOfBytes, false);
- _binReader = new BinaryReader(_buffer, Encoding.Default);
- }
-
- bool disposed = false;
-
- public void Dispose()
- {
- if (disposed)
- return;
- _binReader.Close();
- _buffer.Dispose();
-
- disposed = true;
- }
-
- ///
- /// Restart reading from the point specified.
- ///
- /// The point of the packet to start reading from.
- public void Reset(int length)
- {
- _buffer.Seek(length, SeekOrigin.Begin);
- }
-
- public void Skip(int length)
- {
- _buffer.Position += length;
- }
-
- public int Position
- {
- get { return (int)_buffer.Position; }
- }
- public int Remaining
- {
- get { return Length - (int)_buffer.Position; }
- }
-
- ///
- /// Reads an unsigned byte from the stream
- ///
- /// an unsigned byte from the stream
- public byte ReadByte()
- {
- return _binReader.ReadByte();
- }
-
- ///
- /// Reads a byte array from the stream
- ///
- /// Amount of bytes
- /// A byte array
- public byte[] ReadBytes(int count)
- {
- return _binReader.ReadBytes(count);
- }
-
- ///
- /// Reads a bool from the stream
- ///
- /// A bool
- public bool ReadBool()
- {
- return _binReader.ReadBoolean();
- }
-
- ///
- /// Reads a signed short from the stream
- ///
- /// A signed short
- public short ReadShort()
- {
- return _binReader.ReadInt16();
- }
-
- ///
- /// Reads an unsigned short from the stream
- ///
- /// A signed short
- public ushort ReadUShort()
- {
- return _binReader.ReadUInt16();
- }
-
- ///
- /// Reads a signed int from the stream
- ///
- /// A signed int
- public int ReadInt()
- {
- return _binReader.ReadInt32();
- }
-
- ///
- /// Reads an unsigned int from the stream
- ///
- /// A signed int
- public uint ReadUInt()
- {
- return _binReader.ReadUInt32();
- }
-
- ///
- /// Reads a signed long from the stream
- ///
- /// A signed long
- public long ReadLong()
- {
- return _binReader.ReadInt64();
- }
-
- ///
- /// Reads an unsigned long from the stream
- ///
- /// A signed long
- public ulong ReadULong()
- {
- return _binReader.ReadUInt64();
- }
-
- ///
- /// Reads an ASCII string from the stream
- ///
- /// Amount of bytes
- /// An ASCII string
- public string ReadString(int length)
- {
- return Encoding.Default.GetString(ReadBytes(length));
- }
-
- ///
- /// Reads a maple string from the stream
- ///
- /// A maple string
- public string ReadMapleString()
- {
- return ReadString(ReadShort());
- }
-
- public byte[] ToArray()
- {
- return _buffer.ToArray();
- }
- }
-}
diff --git a/Beanfun/Helper/TextBlockHelper.cs b/Beanfun/Helper/TextBlockHelper.cs
deleted file mode 100644
index 3b36d89..0000000
--- a/Beanfun/Helper/TextBlockHelper.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Documents;
-using System.Windows.Media;
-using System.Xml;
-
-namespace Beanfun
-{
- class TextBlockHelper
- {
- #region FormattedText Attached dependency property
-
- public static string GetFormattedText(DependencyObject obj)
- {
- return (string)obj.GetValue(FormattedTextProperty);
- }
-
- public static void SetFormattedText(DependencyObject obj, string value)
- {
- obj.SetValue(FormattedTextProperty, value);
- }
-
- public static readonly DependencyProperty FormattedTextProperty =
- DependencyProperty.RegisterAttached(
- "FormattedText",
- typeof(string),
- typeof(TextBlockHelper),
- new UIPropertyMetadata("", FormattedTextChanged)
- );
-
- private static void FormattedTextChanged(
- DependencyObject sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- string value = e.NewValue as string;
-
- TextBlock textBlock = sender as TextBlock;
-
- if (textBlock != null)
- {
- textBlock.Inlines.Clear();
- textBlock.Inlines.Add(Process(value));
- }
- }
-
- #endregion
-
- static Inline Process(string value)
- {
- XmlDocument doc = new XmlDocument();
- doc.LoadXml(value);
-
- Span span = new Span();
- InternalProcess(span, doc.FirstChild);
-
- return span;
- }
-
- private static void InternalProcess(Span span, XmlNode xmlNode)
- {
- foreach (XmlNode child in xmlNode)
- {
- Span spanItem = new Span();
- if (child is XmlElement)
- InternalProcess(spanItem, child);
- switch (child.Name.ToUpper())
- {
- case "B":
- case "BOLD":
- Bold bold = new Bold(spanItem);
- span.Inlines.Add(bold);
- break;
- case "I":
- case "ITALIC":
- Italic italic = new Italic(spanItem);
- span.Inlines.Add(italic);
- break;
- case "U":
- case "UNDERLINE":
- Underline underline = new Underline(spanItem);
- span.Inlines.Add(underline);
- break;
- case "L":
- case "LINEBREAK":
- span.Inlines.Add(new LineBreak());
- break;
- case "R":
- case "RUN":
- Run run = new Run(child.InnerText);
- if (child.Attributes != null)
- foreach (XmlNode att in child.Attributes)
- {
- switch (att.Name.ToUpper())
- {
- case "FOREGROUND":
- run.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString(att.Value)
- );
- break;
- case "BACKGROUND":
- run.Background = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString(att.Value)
- );
- break;
- }
- }
- span.Inlines.Add(run);
- break;
- default:
- if (child is XmlText)
- span.Inlines.Add(new Run(child.InnerText));
- break;
- }
- }
- }
- }
-}
diff --git a/Beanfun/Helper/WindowAccentCompositor.cs b/Beanfun/Helper/WindowAccentCompositor.cs
deleted file mode 100644
index 616c617..0000000
--- a/Beanfun/Helper/WindowAccentCompositor.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Runtime.InteropServices;
-using System.Windows;
-using System.Windows.Interop;
-using System.Windows.Media;
-
-namespace Beanfun
-{
- ///
- /// 为窗口提供模糊特效。
- ///
- public class WindowAccentCompositor
- {
- private readonly Window _window;
- private bool _isEnabled;
- private int _blurColor;
-
- ///
- /// 创建 的一个新实例。
- ///
- /// 要创建模糊特效的窗口实例。
- public WindowAccentCompositor(Window window) =>
- _window = window ?? throw new ArgumentNullException(nameof(window));
-
- ///
- /// 获取或设置此窗口模糊特效是否生效的一个状态。
- /// 默认为 false,即不生效。
- ///
- [DefaultValue(false)]
- public bool IsEnabled
- {
- get => _isEnabled;
- set
- {
- _isEnabled = value;
- OnIsEnabledChanged(value);
- }
- }
-
- ///
- /// 获取或设置此窗口模糊特效叠加的颜色。
- ///
- public Color Color
- {
- get =>
- Color.FromArgb(
- // 取出红色分量。
- (byte)((_blurColor & 0x000000ff) >> 0),
- // 取出绿色分量。
- (byte)((_blurColor & 0x0000ff00) >> 8),
- // 取出蓝色分量。
- (byte)((_blurColor & 0x00ff0000) >> 16),
- // 取出透明分量。
- (byte)((_blurColor & 0xff000000) >> 24)
- );
- set =>
- _blurColor =
- // 组装红色分量。
- value.R << 0
- |
- // 组装绿色分量。
- value.G << 8
- |
- // 组装蓝色分量。
- value.B << 16
- |
- // 组装透明分量。
- value.A << 24;
- }
-
- private void OnIsEnabledChanged(bool isEnabled)
- {
- Window window = _window;
- var handle = new WindowInteropHelper(window).EnsureHandle();
- Composite(handle, isEnabled);
- }
-
- private void Composite(IntPtr handle, bool isEnabled)
- {
- // 创建 AccentPolicy 对象。
- var accent = new WindowsAPI.AccentPolicy();
-
- // 设置特效。
- if (!isEnabled)
- {
- accent.AccentState = WindowsAPI.AccentState.ACCENT_DISABLED;
- }
- else if (App.OSVersion >= App.Win11)
- {
- // 如果系统在 Windows 11 以上,则启用亚克力效果,并组合已设置的叠加颜色和透明度。
- // ※從 Windows 10 (1809) 開始已經支持亞克力效果但會造成窗口移動時卡頓問題
- // 请参见《在 WPF 程序中应用 Windows 10 真•亚克力效果》
- // https://blog.walterlv.com/post/using-acrylic-in-wpf-application.html
- accent.AccentState = WindowsAPI.AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND;
- accent.GradientColor = _blurColor;
- }
- else if (App.OSVersion >= App.Win10)
- {
- // 如果系统在 Windows 10 以上,则启用 Windows 10 早期的模糊特效。
- // ※Windows 11 上使用模糊特效會造成窗口移動時卡頓問題
- // 请参见《在 Windows 10 上为 WPF 窗口添加模糊特效》
- // https://blog.walterlv.com/post/win10/2017/10/02/wpf-transparent-blur-in-windows-10.html
- accent.AccentState = WindowsAPI.AccentState.ACCENT_ENABLE_BLURBEHIND;
- }
- else
- {
- // 暂时不处理其他操作系统:
- // - Windows 8/8.1 不支持任何模糊特效
- // - Windows Vista/7 支持 Aero 毛玻璃效果
- return;
- }
-
- // 将托管结构转换为非托管对象。
- var accentPolicySize = Marshal.SizeOf(accent);
- var accentPtr = Marshal.AllocHGlobal(accentPolicySize);
- Marshal.StructureToPtr(accent, accentPtr, false);
-
- // 设置窗口组合特性。
- try
- {
- // 设置模糊特效。
- var data = new WindowsAPI.WindowCompositionAttributeData
- {
- Attribute = WindowsAPI.WindowCompositionAttribute.WCA_ACCENT_POLICY,
- SizeOfData = accentPolicySize,
- Data = accentPtr,
- };
- WindowsAPI.SetWindowCompositionAttribute(handle, ref data);
- }
- finally
- {
- // 释放非托管对象。
- Marshal.FreeHGlobal(accentPtr);
- }
- }
- }
-}
diff --git a/Beanfun/MainWindow.xaml b/Beanfun/MainWindow.xaml
deleted file mode 100644
index 53eb070..0000000
--- a/Beanfun/MainWindow.xaml
+++ /dev/null
@@ -1,255 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/MainWindow.xaml.cs b/Beanfun/MainWindow.xaml.cs
deleted file mode 100644
index 973020a..0000000
--- a/Beanfun/MainWindow.xaml.cs
+++ /dev/null
@@ -1,2761 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Management;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using IniParser.Model;
-using IniParser.Parser;
-using Microsoft.Win32;
-using Newtonsoft.Json.Linq;
-using Utility.ModifyRegistry;
-
-namespace Beanfun
-{
- enum LoginMethod : int
- {
- Regular = 0,
- QRCode = 1,
- GamePass = 2,
- };
-
- enum GameStartMode : int
- {
- Auto = 0,
- Normal = 1,
- LocaleRemulator = 2,
- };
-
- ///
- /// MainWindow.xaml 的交互逻辑
- ///
- public partial class MainWindow : Window
- {
- public LoginPage loginPage;
- public ManageAccount manageAccPage;
- public LoginWait loginWaitPage = new LoginWait();
- public AccountList accountList = null;
- public VerifyPage verifyPage;
- public Settings settingPage;
- public About aboutPage;
- public LoginTotp loginTotp;
-
- public System.ComponentModel.BackgroundWorker getOtpWorker;
- public System.ComponentModel.BackgroundWorker loginWorker;
- public System.ComponentModel.BackgroundWorker totpWorker;
- public System.ComponentModel.BackgroundWorker pingWorker;
- public System.ComponentModel.BackgroundWorker qrWorker;
- public System.ComponentModel.BackgroundWorker verifyWorker;
- public System.Windows.Threading.DispatcherTimer qrCheckLogin;
- public System.Windows.Threading.DispatcherTimer checkPlayPage;
- public System.Windows.Threading.DispatcherTimer checkPatcher;
- public System.Windows.Threading.DispatcherTimer bfAPPAutoLogin;
-
- public AccountManager accountManager = null;
-
- public BeanfunClient bfClient;
- private readonly object _bfClientLock = new object();
-
- public BeanfunClient.QRCodeClass qrcodeClass;
-
- public string LastLoginAccountID = "";
- public string service_code = "610074",
- service_region = "T9";
- public string game_exe = "MapleStory.exe";
- public string dir_value_name = "Path";
- public string win_class_name = "MapleStoryClass";
- public short login_action_type = 1;
- public string game_commandLine = "tw.login.maplestory.beanfun.com 8484 BeanFun %s %s";
- private string otp;
- private BitmapImage qr_default;
- private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(MainWindow));
- private static readonly System.Windows.Forms.NotifyIcon _trayNotifyIcon =
- new System.Windows.Forms.NotifyIcon
- {
- Icon = Properties.Resources.icon,
- Text = Application.Current.TryFindResource("AppName") as string,
- };
-
- public Dictionary> GameList =
- new Dictionary>();
- public GameService SelectedGame = null;
- public bool UnconnectedGame = false;
-
- public string viewstate,
- eventvalidation,
- samplecaptcha;
-
- public Page return_page = null;
- public IniData INIData = null;
-
- public WindowAccentCompositor compositor = null;
-
- public MainWindow()
- {
- InitializeComponent();
-
- this.getOtpWorker = new System.ComponentModel.BackgroundWorker();
- this.loginWorker = new System.ComponentModel.BackgroundWorker();
- this.totpWorker = new System.ComponentModel.BackgroundWorker();
- this.pingWorker = new System.ComponentModel.BackgroundWorker();
- this.qrWorker = new System.ComponentModel.BackgroundWorker();
- this.verifyWorker = new System.ComponentModel.BackgroundWorker();
- this.qrCheckLogin = new System.Windows.Threading.DispatcherTimer();
- this.checkPlayPage = new System.Windows.Threading.DispatcherTimer();
- this.checkPatcher = new System.Windows.Threading.DispatcherTimer();
- this.bfAPPAutoLogin = new System.Windows.Threading.DispatcherTimer();
- //
- // getOtpWorker
- //
- this.getOtpWorker.WorkerReportsProgress = true;
- this.getOtpWorker.WorkerSupportsCancellation = true;
- this.getOtpWorker.DoWork += this.getOtpWorker_DoWork;
- this.getOtpWorker.RunWorkerCompleted += this.getOtpWorker_RunWorkerCompleted;
- //
- // loginWorker
- //
- this.loginWorker.WorkerReportsProgress = true;
- this.loginWorker.WorkerSupportsCancellation = true;
- this.loginWorker.DoWork += this.loginWorker_DoWork;
- this.loginWorker.RunWorkerCompleted += this.loginWorker_RunWorkerCompleted;
- //
- // totpWorker
- //
- this.totpWorker.WorkerReportsProgress = true;
- this.totpWorker.WorkerSupportsCancellation = true;
- this.totpWorker.DoWork += this.totpWorker_DoWork;
- this.totpWorker.RunWorkerCompleted += this.totpWorker_RunWorkerCompleted;
- //
- // pingWorker
- //
- this.pingWorker.WorkerReportsProgress = true;
- this.pingWorker.WorkerSupportsCancellation = true;
- this.pingWorker.DoWork += this.pingWorker_DoWork;
- this.pingWorker.RunWorkerCompleted += this.pingWorker_RunWorkerCompleted;
- //
- // qrWorker
- //
- this.qrWorker.WorkerReportsProgress = true;
- this.qrWorker.WorkerSupportsCancellation = true;
- this.qrWorker.DoWork += this.qrWorker_DoWork;
- this.qrWorker.RunWorkerCompleted += this.qrWorker_RunWorkerCompleted;
- //
- // verifyWorker
- //
- this.verifyWorker.WorkerReportsProgress = true;
- this.verifyWorker.WorkerSupportsCancellation = true;
- this.verifyWorker.DoWork += this.verifyWorker_DoWork;
- this.verifyWorker.RunWorkerCompleted += this.verifyWorker_RunWorkerCompleted;
- //
- // qrCheckLogin
- //
- this.qrCheckLogin.Interval = TimeSpan.FromSeconds(2);
- this.qrCheckLogin.Tick += this.qrCheckLogin_Tick;
- //
- // checkPlayPage
- //
- this.checkPlayPage.Interval = TimeSpan.FromMilliseconds(100);
- this.checkPlayPage.Tick += this.checkPlayPage_Tick;
- //
- // checkPatcher
- //
- this.checkPatcher.Interval = TimeSpan.FromMilliseconds(100);
- this.checkPatcher.Tick += this.checkPatcher_Tick;
- //
- // bfAPPAutoLogin
- //
- this.bfAPPAutoLogin.Interval = TimeSpan.FromSeconds(2);
- this.bfAPPAutoLogin.Tick += this.bfAPPAutoLogin_Tick;
-
- loginPage = new LoginPage();
- manageAccPage = new ManageAccount();
- verifyPage = new VerifyPage();
- accountList = new AccountList();
- settingPage = new Settings();
- aboutPage = new About();
- loginTotp = new LoginTotp();
-
- Initialize();
-
- if (
- (App.OSVersion >= App.Win7 && App.OSVersion < App.Win8)
- || App.OSVersion >= App.Win10
- )
- {
- compositor = new WindowAccentCompositor(this);
- if (App.OSVersion >= App.Win7 && App.OSVersion < App.Win8)
- {
- WinChrome.GlassFrameThickness = new Thickness(-1);
-
- const int GWL_STYLE = -16;
- const int WS_SYSMENU = 0x80000;
- var hwnd = new System.Windows.Interop.WindowInteropHelper(this).EnsureHandle();
- WindowsAPI.SetWindowLong(
- hwnd,
- GWL_STYLE,
- WindowsAPI.GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU
- );
- }
- }
- else
- frame.Content = loginWaitPage;
-
- changeThemeColor(null);
- }
-
- protected override void OnContentRendered(EventArgs e)
- {
- frame.Content = loginPage;
- //frame.Content = loginTotp;
-
- if (
- App.LoginMethod == (int)LoginMethod.Regular
- && (bool)loginPage.id_pass.checkBox_AutoLogin.IsChecked
- )
- {
- do_Login();
- }
-
- base.OnContentRendered(e);
- }
-
- public void NavigateLoginPage()
- {
- frame.Content = loginPage;
-
- btn_Region.Visibility = Visibility.Visible;
-
- try
- {
- if (bfClient != null)
- bfClient.Logout();
- }
- catch { }
- }
-
- public void changeThemeColor(string sColor)
- {
- if (sColor == null)
- sColor = ConfigAppSettings.GetValue("ThemeColor", "#FF8201");
- Color color = (Color)ColorConverter.ConvertFromString(sColor);
- bool oldIsLightColor = isLightColor();
- Background = new SolidColorBrush(color);
- color.R = (byte)Math.Max(color.R - 50, 0);
- color.G = (byte)Math.Max(color.G - 50, 0);
- color.B = (byte)Math.Max(color.B - 50, 0);
- color.A = 0xFF;
- this.BorderBrush = new SolidColorBrush(color);
-
- // Update theme color resource for ListBox selection
- Application.Current.Resources["ThemeColorBrush"] = new SolidColorBrush(color);
- bool isLightMode = isLightColor();
- if (compositor != null)
- {
- int bgA = -1;
- if (!this.IsActive)
- {
- compositor.IsEnabled = false;
- if (App.OSVersion >= App.Win7 && App.OSVersion < App.Win8)
- bgA = 0;
- }
- else
- {
- bgA =
- App.OSVersion < App.Win8 ? 0x4C
- : App.OSVersion < App.Win11 ? 0xCC
- : 0x99;
- compositor.Color = (Color)
- ColorConverter.ConvertFromString(
- isLightMode || ((SolidColorBrush)Background).Color == Colors.Black
- ? "#00FFFFFF"
- : "#00000000"
- );
- if (!compositor.IsEnabled)
- compositor.IsEnabled = true;
- }
- if (bgA != -1)
- {
- Color bg = ((SolidColorBrush)Background).Color;
- bg.A = (byte)bgA;
- Background = new SolidColorBrush(bg);
- }
- }
- if (oldIsLightColor != isLightMode)
- {
- btn_About_MouseLeave(null, null);
- btn_Setting_MouseLeave(null, null);
- btn_Region_MouseLeave(null, null);
- btn_Min_MouseLeave(null, null);
- btn_Close_MouseLeave(null, null);
- LogoIcon.Fill = new SolidColorBrush(isLightMode ? Colors.Black : Colors.White);
- if (aboutPage != null)
- aboutPage.initThemeColor(isLightMode);
- }
- }
-
- public bool isLightColor()
- {
- Color color = ((SolidColorBrush)Background).Color;
- return (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255 > 0.5;
- }
-
- public Color getTitleButtonColor()
- {
- return (Color)ColorConverter.ConvertFromString(isLightColor() ? "Black" : "White");
- }
-
- public void Initialize()
- {
- try
- {
- if (App.OSVersion < App.Win11)
- {
- ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
- }
- // Allow SSL errors for beanfun/gamania domains; fall through for unknown sender types
- // (e.g. process-mode game accelerators like UU) to preserve backward compatibility
- ServicePointManager.ServerCertificateValidationCallback = (
- sender,
- certificate,
- chain,
- errors
- ) =>
- {
- if (errors == System.Net.Security.SslPolicyErrors.None)
- return true;
- if (sender is HttpWebRequest req)
- {
- string host = req.RequestUri.Host;
- return host.EndsWith(".beanfun.com") || host.EndsWith(".gamania.com");
- }
- return true;
- };
- if (settingPage.tradLogin != null && !(bool)settingPage.tradLogin.IsChecked)
- accountList.panel_GetOtp.Visibility = Visibility.Collapsed;
-
- qr_default = new BitmapImage();
- qr_default.BeginInit();
- qr_default.UriSource = new Uri("pack://application:,,,/Resources/refresh.png");
- qr_default.EndInit();
- loginPage.qr.qr_image.Source = qr_default;
-
- string loginGame = ConfigAppSettings.GetValue("loginGame", "");
- if (loginGame != "")
- {
- string[] arr = loginGame.Split('_');
- if (arr != null && arr.Length > 1)
- {
- service_code = arr[0];
- service_region = arr[1];
- }
- }
-
- if ((bool)settingPage.ask_update.IsChecked)
- {
- new Thread(() => CheckUpdates(false)).Start();
- }
-
- this.accountManager = new AccountManager();
-
- bool res = accountManager.init();
- if (res == false)
- errexit(TryFindResource("InitAccountError") as string, 0);
-
- settingPage.t_GamePath.PreviewMouseLeftButtonDown += this.btn_SetGamePath_Click;
- LastLoginAccountID = ConfigAppSettings.GetValue("AccountID", LastLoginAccountID);
- int loginMethod = accountManager.getMethodByAccount(
- App.LoginRegion,
- LastLoginAccountID
- );
- if (loginMethod < (int)LoginMethod.Regular)
- loginMethod = int.Parse(ConfigAppSettings.GetValue("loginMethod", "0"));
- // Don't restore QRCode/GamePass on startup — they require active auth sessions
- loginMethod = Math.Min(
- loginMethod,
- App.LoginRegion == "TW" ? (int)LoginMethod.QRCode : (int)LoginMethod.Regular
- );
-
- loginMethodInit();
-
- Dispatcher.BeginInvoke(new Action(() => reLoadGameInfo()));
-
- App.LoginMethod = loginMethod;
- loginMethodChanged();
-
- _trayNotifyIcon.MouseClick += (sender, e) =>
- {
- if (e.Button == System.Windows.Forms.MouseButtons.Left)
- {
- this.Visibility = Visibility.Visible;
- _trayNotifyIcon.Visible = false;
- }
- };
-
- frame.Content = loginPage;
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.StackTrace);
- MessageBox.Show(
- string.Format(
- Regex.Unescape(TryFindResource("LoadDataError") as string),
- ex.Message
- ) /* + "\r\n\r\n" + ex.StackTrace*/
- );
-
- new LoginRegionSelection().ShowDialog();
- }
- }
-
- public class GameService
- {
- public string name { get; set; }
- public string service_code { get; set; }
- public string service_region { get; set; }
- public string website_url { get; set; }
- public string xlarge_image_name { get; set; }
- public string large_image_name { get; set; }
- public string small_image_name { get; set; }
- public string download_url { get; set; }
-
- private string imageBaseUrl
- {
- get
- {
- return App.LoginRegion == "TW"
- ? "https://tw.images.beanfun.com/uploaded_images/beanfun_tw/game_zone/"
- : "http://hk.images.beanfun.com/uploaded_images/beanfun/game_zone/";
- }
- }
-
- private BitmapImage xlarge_image;
- public BitmapImage XLarge_image
- {
- get
- {
- if (xlarge_image == null)
- xlarge_image = loadImage(large_image_name);
- return xlarge_image;
- }
- }
-
- private BitmapImage large_image;
- public BitmapImage Large_image
- {
- get
- {
- if (large_image == null)
- large_image = loadImage(large_image_name);
- return large_image;
- }
- }
-
- private BitmapImage small_image;
- public BitmapImage Small_image
- {
- get
- {
- if (small_image == null)
- small_image = loadImage(small_image_name);
- return small_image;
- }
- }
-
- public GameService(
- string name,
- string service_code,
- string service_region,
- string website_url,
- string xlarge_image_name,
- string large_image_name,
- string small_image_name,
- string download_url
- )
- {
- this.name = I18n.ToSimplified(name);
- this.service_code = service_code;
- this.service_region = service_region;
- this.website_url = website_url;
- this.xlarge_image_name = xlarge_image_name;
- this.large_image_name = large_image_name;
- this.small_image_name = small_image_name;
- this.download_url = download_url;
- }
-
- private BitmapImage loadImage(string url)
- {
- if (
- !url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
- && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
- )
- {
- url = $"{imageBaseUrl}{url}";
- }
- BitmapImage image;
- try
- {
- byte[] buffer = new WebClient().DownloadData(url);
- image = new BitmapImage();
- image.BeginInit();
- image.StreamSource = new MemoryStream(buffer);
- image.EndInit();
- }
- catch (Exception)
- {
- image = null;
- }
- return image;
- }
- }
-
- public void selectedGameChanged()
- {
- string gameCode = service_code + "_" + service_region;
- ConfigAppSettings.SetValue("loginGame", gameCode);
-
- if (INIData == null)
- {
- reLoadGameInfo();
- return;
- }
- string exe = INIData[gameCode]["exe"];
- if (exe == null)
- {
- new GameList().ShowDialog();
- return;
- }
- Regex regex = new Regex("(.*).exe");
- if (regex.IsMatch(exe))
- game_exe = regex.Match(exe).Groups[1].Value + ".exe";
- else
- game_exe = "";
- regex = new Regex(".exe (.*)");
- if (regex.IsMatch(exe))
- game_commandLine = regex.Match(exe).Groups[1].Value;
- else
- game_commandLine = "";
-
- login_action_type = 8;
- string sLoginActionType = INIData[gameCode]["login_action_type"];
- if (sLoginActionType != "")
- login_action_type = short.Parse(sLoginActionType);
- if (login_action_type == 1)
- {
- settingPage.tradLogin.Visibility = Visibility.Visible;
- if ((bool)settingPage.tradLogin.IsChecked)
- accountList.panel_GetOtp.Visibility = Visibility.Visible;
- else
- accountList.panel_GetOtp.Visibility = Visibility.Collapsed;
- }
- else
- {
- settingPage.tradLogin.Visibility = Visibility.Collapsed;
- accountList.panel_GetOtp.Visibility = Visibility.Visible;
- }
-
- win_class_name = INIData[gameCode]["win_class_name"];
- if ("MapleStoryClass".Equals(win_class_name))
- {
- accountList.autoPaste.Visibility = Visibility.Visible;
- }
- else
- {
- accountList.autoPaste.Visibility = Visibility.Collapsed;
- }
- dir_value_name = INIData[gameCode]["dir_value_name"];
- if (ConfigAppSettings.GetValue(dir_value_name + "." + gameCode, "") == "")
- {
- string dir_reg = INIData[gameCode]["dir_reg"];
- if (dir_reg != "")
- {
- dir_reg = dir_reg.Replace("HKEY_LOCAL_MACHINE\\", "");
-
- try
- {
- ModifyRegistry myRegistry = new ModifyRegistry();
- myRegistry.BaseRegistryKey = Registry.CurrentUser;
- myRegistry.SubKey = dir_reg;
- if (myRegistry.Read(dir_value_name) != "")
- {
- ConfigAppSettings.SetValue(
- dir_value_name + "." + gameCode,
- myRegistry.Read(dir_value_name)
- );
- settingPage.t_GamePath.Text = myRegistry.Read(dir_value_name);
- }
- }
- catch
- {
- settingPage.t_GamePath.Text = "";
- }
- }
- }
- else
- {
- settingPage.t_GamePath.Text = ConfigAppSettings.GetValue(
- dir_value_name + "." + gameCode
- );
- }
-
- if (gameCode == "610074_T9" || gameCode == "610075_T9")
- {
- settingPage.skipPlayWnd.Visibility = Visibility.Visible;
-
- if ((bool)settingPage.skipPlayWnd.IsChecked)
- checkPlayPage.IsEnabled = true;
-
- settingPage.autoKillPatcher.Visibility = Visibility.Visible;
-
- if ((bool)settingPage.autoKillPatcher.IsChecked)
- checkPatcher.IsEnabled = true;
-
- settingPage.btn_Tools.Visibility = Visibility.Visible;
- }
- else
- {
- settingPage.skipPlayWnd.Visibility = Visibility.Collapsed;
- checkPlayPage.IsEnabled = false;
- settingPage.autoKillPatcher.Visibility = Visibility.Collapsed;
- checkPatcher.IsEnabled = false;
-
- if (gameCode == "610096_TE")
- settingPage.btn_Tools.Visibility = Visibility.Visible;
- else
- settingPage.btn_Tools.Visibility = Visibility.Collapsed;
- }
-
- if (this.bfClient != null && !loginWorker.IsBusy && !getOtpWorker.IsBusy)
- {
- this.bfClient.GetAccounts(service_code, service_region);
- redrawSAccountList();
- if (this.bfClient.errmsg != null)
- {
- errexit(this.bfClient.errmsg, 2);
- this.bfClient.errmsg = null;
- }
- }
- switch (gameCode)
- {
- case "610153_TN":
- case "610085_TC":
- UnconnectedGame = true;
- break;
- default:
- UnconnectedGame = false;
- break;
- }
-
- try
- {
- if (loginPage != null)
- {
- foreach (GameService gs in GameList[App.LoginRegion.ToLower()])
- {
- if (
- gs.service_region == this.service_region
- && gs.service_code == this.service_code
- )
- {
- loginPage.id_pass.imageGame.ImageSource = gs.Large_image;
- accountList.imageGame.Source = gs.Small_image;
- accountList.gameName.Content = gs.name;
- SelectedGame = gs;
- break;
- }
- }
- }
- }
- catch
- { /* ignore out of range */
- }
- }
-
- public void reLoadGameInfo()
- {
- if (!GameList.ContainsKey(App.LoginRegion.ToLower()))
- {
- var capturedRegion = App.LoginRegion.ToLower();
- string host = capturedRegion == "hk" ? "bfweb.hk" : "tw";
- new Thread(() =>
- {
- try
- {
- List gameList = new List();
- WebClient wc = new WebClient();
-
- string res = Encoding.UTF8.GetString(
- wc.DownloadData(
- $"https://{host}.beanfun.com/beanfun_block/generic_handlers/get_service_ini.ashx"
- )
- );
-
- IniDataParser parser = new IniDataParser();
- var iniData = parser.Parse(res);
-
- res = Encoding.UTF8.GetString(
- wc.DownloadData($"https://{host}.beanfun.com/game_zone/")
- );
- Regex reg = new Regex("Services\\.ServiceList = (.*);");
- if (reg.IsMatch(res))
- {
- string json = reg.Match(res).Groups[1].Value;
- bool newJson = new Regex("^\\[(.*)\\]$").IsMatch(json);
- if (newJson)
- {
- JArray jsons = JArray.Parse(json);
- foreach (JObject game in jsons)
- AddGameServiceFromJson(gameList, game);
- }
- else
- {
- JObject o = JObject.Parse(json);
- foreach (JObject game in o["Rows"])
- AddGameServiceFromJson(gameList, game);
- }
- }
-
- Dispatcher.Invoke(() =>
- {
- if (App.LoginRegion.ToLower() != capturedRegion)
- return;
-
- INIData = iniData;
- if (!GameList.ContainsKey(capturedRegion))
- GameList.Add(capturedRegion, gameList);
- selectedGameChanged();
- });
- }
- catch (Exception ex)
- {
- log.Error("reLoadGameInfo failed", ex);
- }
- })
- {
- IsBackground = true,
- Name = "reLoadGameInfo",
- }.Start();
- return;
- }
-
- selectedGameChanged();
- }
-
- private void AddGameServiceFromJson(List gameList, JObject game)
- {
- GameService gs = new GameService(
- (string)game["ServiceFamilyName"],
- (string)game["ServiceCode"],
- (string)game["ServiceRegion"],
- (string)game["ServiceWebsiteURL"],
- (string)game["ServiceXLargeImageName"],
- (string)game["ServiceLargeImageName"],
- (string)game["ServiceSmallImageName"],
- (string)game["ServiceDownloadURL"]
- );
- gameList.Add(gs);
- if (gs.service_code == service_code && gs.service_region == service_region)
- SelectedGame = gs;
- }
-
- public void CheckUpdates(bool show)
- {
- Update.ApplicationUpdater.CheckApplicationUpdate(show);
- }
-
- private string reLoadVerifyPage(string response)
- {
- Regex regex;
-
- // __VIEWSTATE
- regex = new Regex("id=\"__VIEWSTATE\"[^>]+value=\"([^\"]+)\"");
- if (!regex.IsMatch(response))
- {
- return "VerifyNoViewstate";
- }
- this.viewstate = regex.Match(response).Groups[1].Value;
-
- // __VIEWSTATEGENERATOR (optional but store if present)
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\"[^>]+value=\"([^\"]+)\"");
- if (regex.IsMatch(response))
- {
- this.bfClient.verifyViewStateGenerator = regex.Match(response).Groups[1].Value;
- }
-
- // __EVENTVALIDATION
- regex = new Regex("id=\"__EVENTVALIDATION\"[^>]+value=\"([^\"]+)\"");
- if (!regex.IsMatch(response))
- {
- return "VerifyNoEventvalidation";
- }
- this.eventvalidation = regex.Match(response).Groups[1].Value;
-
- // Captcha ID
- regex = new Regex("id=\"LBD_VCID_[^\"]+\"[^>]+value=\"([^\"]+)\"");
- if (!regex.IsMatch(response))
- {
- return "VerifyNoSamplecaptcha";
- }
- this.samplecaptcha = regex.Match(response).Groups[1].Value;
-
- // Auth type label
- regex = new Regex("id=\"lblAuthType\">([^<]+)<");
- if (!regex.IsMatch(response))
- {
- return "VerifyNoLblAuthType";
- }
- verifyPage.labelAuthType.Content = regex.Match(response).Groups[1].Value;
-
- // Form action URL (store for submit)
- regex = new Regex("action=\"(AdvanceCheck\\.aspx[^\"]+)\"");
- if (regex.IsMatch(response))
- {
- string formAction = regex.Match(response).Groups[1].Value.Replace("&", "&");
- this.bfClient.verifyFormAction =
- $"https://tw.newlogin.beanfun.com/LoginCheck/{formAction}";
- }
-
- // Alert check
- regex = new Regex("alert\\('(.*)'\\);");
- if (regex.IsMatch(response))
- {
- return regex.Match(response).Groups[1].Value;
- }
-
- verifyPage.imageCaptcha.Source = this.bfClient.getVerifyCaptcha(this.samplecaptcha);
- return null;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- try
- {
- this.DragMove();
- }
- catch { }
- }
-
- private void Window_Activated(object sender, EventArgs e)
- {
- btn_About_MouseLeave(null, null);
- btn_Setting_MouseLeave(null, null);
- btn_Region_MouseLeave(null, null);
- btn_Min_MouseLeave(null, null);
- btn_Close_MouseLeave(null, null);
- if (this.IsActive)
- {
- changeThemeColor(null);
- }
- else
- {
- changeThemeColor("#F3F3F3");
- this.BorderBrush = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
- }
-
- private void Window_StateChanged(object sender, EventArgs e)
- {
- if (
- settingPage != null
- && settingPage.minimize_to_tray != null
- && (bool)settingPage.minimize_to_tray.IsChecked
- && this.WindowState == WindowState.Minimized
- )
- {
- this.WindowState = WindowState.Normal;
- this.Visibility = Visibility.Hidden;
- _trayNotifyIcon.Visible = true;
- }
- }
-
- private void btn_About_MouseLeave(object sender, MouseEventArgs e)
- {
- if (this.IsActive)
- btn_About.Foreground = new SolidColorBrush(getTitleButtonColor());
- else
- btn_About.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
-
- private void btn_About_IsKeyboardFocusedChanged(
- object sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- if (btn_About.IsKeyboardFocused)
- frame.Focus();
- }
-
- private void btn_Setting_MouseLeave(object sender, MouseEventArgs e)
- {
- if (this.IsActive)
- btn_Setting.Foreground = new SolidColorBrush(getTitleButtonColor());
- else
- btn_Setting.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
-
- private void btn_Setting_IsKeyboardFocusedChanged(
- object sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- if (btn_Setting.IsKeyboardFocused)
- frame.Focus();
- }
-
- private void CloseGamePassBrowser()
- {
- foreach (Window wnd in Application.Current.Windows)
- {
- if (wnd is GamePassBrowser)
- {
- wnd.Close();
- break;
- }
- }
- }
-
- private void btn_Region_Click(object sender, RoutedEventArgs e)
- {
- loginPage.qr.CloseEnlargeWindow();
- CloseGamePassBrowser();
- App.LoginRegion = App.LoginRegion == "TW" ? "HK" : "TW";
- ConfigAppSettings.SetValue("loginRegion", App.LoginRegion);
- loginMethodInit();
- reLoadGameInfo();
- }
-
- private void btn_Region_MouseLeave(object sender, MouseEventArgs e)
- {
- if (this.IsActive)
- btn_Region.Foreground = new SolidColorBrush(getTitleButtonColor());
- else
- btn_Region.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
-
- private void btn_Region_IsKeyboardFocusedChanged(
- object sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- if (btn_Region.IsKeyboardFocused)
- frame.Focus();
- }
-
- private void btn_Min_MouseLeave(object sender, MouseEventArgs e)
- {
- if (this.IsActive)
- btn_Min.Foreground = new SolidColorBrush(getTitleButtonColor());
- else
- btn_Min.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
-
- private void btn_Min_IsKeyboardFocusedChanged(
- object sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- if (btn_Min.IsKeyboardFocused)
- frame.Focus();
- }
-
- private void btn_Close_MouseEnter(object sender, MouseEventArgs e)
- {
- btn_Close.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("White")
- );
- }
-
- private void btn_Close_MouseLeave(object sender, MouseEventArgs e)
- {
- if (this.IsActive)
- btn_Close.Foreground = new SolidColorBrush(getTitleButtonColor());
- else
- btn_Close.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("Gray")
- );
- }
-
- private void btn_Close_IsKeyboardFocusedChanged(
- object sender,
- DependencyPropertyChangedEventArgs e
- )
- {
- if (btn_Close.IsKeyboardFocused)
- frame.Focus();
- }
-
- private void btn_About_Click(object sender, RoutedEventArgs e)
- {
- frame.Content = aboutPage;
- if (return_page != null)
- return;
- return_page = (Page)frame.Content;
- }
-
- private void btn_Setting_Click(object sender, RoutedEventArgs e)
- {
- frame.Content = settingPage;
- if (return_page != null)
- return;
- return_page = (Page)frame.Content;
- }
-
- private void btn_Min_Click(object sender, RoutedEventArgs e)
- {
- base.WindowState = WindowState.Minimized;
- }
-
- private void btn_Close_Click(object sender, RoutedEventArgs e)
- {
- System.Windows.Application.Current.Shutdown();
- }
-
- private void btn_SetGamePath_Click(object sender, RoutedEventArgs e)
- {
- string gameCode = service_code + "_" + service_region;
- OpenFileDialog openFileDialog = new OpenFileDialog();
- openFileDialog.Filter =
- accountList.gameName.Content
- + string.Format(TryFindResource("FileDialog_Filter") as string, game_exe);
- openFileDialog.Title = string.Format(
- TryFindResource("FileDialog_Title") as string,
- game_exe
- );
-
- if (openFileDialog.ShowDialog() == true)
- {
- string file = openFileDialog.FileName;
- ConfigAppSettings.SetValue(dir_value_name + "." + gameCode, file);
- settingPage.t_GamePath.Text = file;
- }
- }
-
- public void loginMethodChanged()
- {
- if (qrWorker.IsBusy)
- qrWorker.CancelAsync();
- loginPage.qr.CloseEnlargeWindow();
- CloseGamePassBrowser();
- qrCheckLogin.IsEnabled = false;
- btn_Region.IsEnabled = true;
- settingPage.LoginModePanel.Visibility =
- (App.LoginRegion == "TW") ? Visibility.Visible : Visibility.Collapsed;
- if (App.LoginRegion == "TW")
- {
- loginPage.id_pass.btn_GamePass.Visibility = Visibility.Visible;
- switch (App.LoginMethod)
- {
- case (int)LoginMethod.QRCode:
- btn_Region.IsEnabled = false;
- loginPage.qr.qr_image.Source = qr_default;
- loginPage.login_form.Content = loginPage.qr;
- if (!qrWorker.IsBusy)
- qrWorker.RunWorkerAsync(
- loginPage == null || loginPage.qr == null ? false : true
- );
- break;
- case (int)LoginMethod.GamePass:
- btn_Region.IsEnabled = false;
- loginPage.login_form.Content = loginPage.gamepass;
- break;
- default:
- loginPage.login_form.Content = loginPage.id_pass;
- break;
- }
- }
- else
- {
- loginPage.id_pass.btn_GamePass.Visibility = Visibility.Collapsed;
- loginPage.login_form.Content = loginPage.id_pass;
- App.LoginMethod = (int)LoginMethod.Regular;
- }
-
- if (
- App.LoginMethod == (int)Beanfun.LoginMethod.Regular
- && (
- loginPage.id_pass.t_Password.Password == ""
- || loginPage.id_pass.t_Password.Password == null
- )
- )
- {
- string pwd = accountManager.getPasswordByAccount(
- App.LoginRegion,
- loginPage.id_pass.t_AccountID.Text
- );
-
- if (pwd != null && pwd != "")
- {
- loginPage.id_pass.t_Password.Password = pwd;
- loginPage.id_pass.checkBox_RememberPWD.IsChecked = true;
- loginPage.id_pass.checkBox_AutoLogin.IsChecked =
- accountManager.getAutoLoginByAccount(
- App.LoginRegion,
- loginPage.id_pass.t_AccountID.Text
- );
- }
-
- string verify = accountManager.getVerifyByAccount(
- App.LoginRegion,
- loginPage.id_pass.t_AccountID.Text
- );
- if (verify != null && verify != "")
- {
- verifyPage.t_Verify.Text = verify;
- verifyPage.checkBoxRememberVerify.IsChecked = true;
- }
- else
- {
- verifyPage.t_Verify.Text = "";
- verifyPage.checkBoxRememberVerify.IsChecked = false;
- }
- }
- }
-
- public void loginMethodInit()
- {
- try
- {
- if (App.LoginRegion == "TW")
- {
- btn_Region.Content = "TW";
- btn_Region.ToolTip = TryFindResource("ChangHKRegion") as string;
- loginPage.id_pass.btn_QRCode.IsEnabled = true;
-
- accountList.btn_Deposite.Visibility = Visibility.Visible;
- }
- else
- {
- btn_Region.Content = "HK";
- btn_Region.ToolTip = TryFindResource("ChangTWRegion") as string;
- loginPage.id_pass.btn_QRCode.IsEnabled = false;
-
- accountList.btn_Deposite.Visibility = Visibility.Collapsed;
- }
- }
- catch { }
-
- try
- {
- string accId = LastLoginAccountID;
- int selectedIndex = -1;
- string[] accountArrays = accountManager.getAccountList(App.LoginRegion);
- List accList = new List();
-
- int i = 0;
- foreach (string s in accountArrays)
- {
- if (s == accId)
- selectedIndex = i;
- string name = accountManager.getNameByAccount(App.LoginRegion, s);
- if (name != null && name != "")
- {
- accList.Add(name + "(" + s + ")");
- }
- else
- {
- accList.Add(s);
- }
- i++;
- }
- loginPage.id_pass.t_AccountID.ItemsSource = null;
- loginPage.id_pass.t_AccountID.ItemsSource = accList;
-
- int loginMethod = accountManager.getMethodByAccount(App.LoginRegion, accId);
- if (loginMethod < (int)LoginMethod.Regular)
- {
- if (accountArrays.Length > 0)
- {
- accId = accList[0];
- selectedIndex = 0;
- }
- loginMethod = accountManager.getMethodByAccount(App.LoginRegion, accId);
- }
-
- if (loginMethod > -1)
- {
- loginPage.id_pass.t_AccountID.SelectedIndex = selectedIndex;
-
- App.LoginMethod = loginMethod;
- loginMethodChanged();
-
- string pwd = accountManager.getPasswordByAccount(App.LoginRegion, accId);
- if (loginMethod != (int)LoginMethod.Regular)
- pwd = "";
-
- if (pwd == null || pwd == "")
- {
- loginPage.id_pass.t_Password.Password = "";
- loginPage.id_pass.checkBox_RememberPWD.IsChecked = false;
- loginPage.id_pass.checkBox_AutoLogin.IsChecked = false;
- }
- }
- else
- {
- loginPage.id_pass.t_AccountID.Text = "";
- loginPage.id_pass.t_Password.Password = "";
- loginPage.id_pass.checkBox_RememberPWD.IsChecked = false;
- loginPage.id_pass.checkBox_AutoLogin.IsChecked = false;
-
- App.LoginMethod = (int)LoginMethod.Regular;
- loginMethodChanged();
-
- verifyPage.t_Verify.Text = "";
- verifyPage.checkBoxRememberVerify.IsChecked = false;
- }
- }
- catch
- { /* ignore out of range */
- }
- manageAccPage.setupAccList(this);
- }
-
- public void do_Login()
- {
- btn_Region.Visibility = Visibility.Collapsed;
- this.loginWorker.RunWorkerAsync(App.LoginMethod);
- frame.Content = loginWaitPage;
- }
-
- public void do_Totp()
- {
- btn_Region.Visibility = Visibility.Collapsed;
- frame.Content = loginWaitPage;
- this.totpWorker.RunWorkerAsync();
- }
-
- public bool errexit(string msg, int method, string title = null)
- {
- switch (msg)
- {
- case "AdvanceCheckSuccessRetry":
- msg = TryFindResource("AdvanceCheckSuccessRetry") as string;
- method = 1;
- break;
- case "LoginNoResponse":
- case "LoginNoSkey":
- case "LoginNoOTP1":
- case "LoginNoSeed":
- case "LoginNoHash":
- case "LoginIntResultError":
- case "AKeyParseFailed":
- case "authkeyParseFailed":
- case "LoginUnknown":
- msg = TryFindResource(msg) as string;
- method = 1;
- break;
- case "LoginNoAkey":
- msg = $"{TryFindResource("LoginNoAkey") as string}({msg})";
- break;
- case "LoginNoAccountMatch":
- case "LoginGetAccountErr":
- case "LoginUpdateAccountListErr":
- msg = $"{TryFindResource("LoginNoAccountMatch") as string}({msg})";
- break;
- case "MainAccount_Not_Exist":
- msg = string.Format(
- TryFindResource("MainAccount_Not_Exist") as string,
- App.LoginRegion == "TW"
- ? TryFindResource("Taiwan")
- : TryFindResource("HongKong")
- );
- break;
- default:
- if (msg.StartsWith("OTPNoLongPollingKey:"))
- {
- string otpDetail = msg.Substring("OTPNoLongPollingKey:".Length);
- if (string.IsNullOrEmpty(otpDetail))
- msg = TryFindResource("GetOtpInitError") as string;
- else if (otpDetail.Contains("很抱歉,需先完成進階認證"))
- msg = TryFindResource("NeedAuthToPlayGame") as string;
- else if (
- otpDetail.Contains("尚未登入,請重新登入")
- || otpDetail.Contains("無法認證登入狀態")
- )
- {
- msg = TryFindResource("DisconnectedFromServer") as string;
- method = 1;
- }
- }
- else
- {
- string localized = null;
- try
- {
- localized = TryFindResource(msg) as string;
- }
- catch { }
- if (localized != null)
- msg = localized;
- }
- break;
- }
-
- string displayMsg = I18n.ToSimplified(msg);
- try
- {
- displayMsg = Regex.Unescape(displayMsg);
- }
- catch { }
-
- MessageBox.Show(displayMsg, title ?? TryFindResource("Error") as string);
-
- if (method == 0)
- App.Current.Shutdown();
- else if (method == 1)
- {
- loginMethodChanged();
- if (accountList?.t_Password != null)
- accountList.t_Password.Text = "";
- NavigateLoginPage();
- }
-
- return false;
- }
-
- private volatile bool isCancelRequested;
-
- public void CancelWork()
- {
- isCancelRequested = true;
- }
-
- public void ResumeWork()
- {
- isCancelRequested = false;
- }
-
- private void OnLoginCompleted()
- {
- ConfigAppSettings.SetValue("loginMethod", App.LoginMethod.ToString());
-
- SaveLoginCredentials();
- ShowAccountListPage();
- }
-
- public void GamePassLoginCompleted(
- string webToken,
- System.Collections.Generic.List cookies
- )
- {
- bfClient.GamePassLogin(webToken, cookies, service_code, service_region);
-
- if (bfClient.errmsg != null)
- {
- errexit(bfClient.errmsg, 1);
- return;
- }
-
- App.LoginMethod = (int)LoginMethod.GamePass;
- ConfigAppSettings.SetValue("loginMethod", App.LoginMethod.ToString());
- ShowAccountListPage();
- }
-
- private void SaveLoginCredentials()
- {
- bool isAccountLogin =
- App.LoginRegion != "TW" || App.LoginMethod != (int)LoginMethod.QRCode;
- if (!isAccountLogin)
- {
- ConfigAppSettings.SetValue("AccountID", null);
- return;
- }
-
- var idPassForm = loginPage.id_pass;
- string accountId = idPassForm.t_AccountID.Text;
- LastLoginAccountID = accountId;
- ConfigAppSettings.SetValue("AccountID", accountId);
-
- accountManager.addAccount(
- App.LoginRegion,
- accountId,
- "",
- idPassForm.checkBox_RememberPWD.IsEnabled
- && (bool)idPassForm.checkBox_RememberPWD.IsChecked
- ? idPassForm.t_Password.Password
- : "",
- (bool)verifyPage.checkBoxRememberVerify.IsChecked ? verifyPage.t_Verify.Text : "",
- App.LoginMethod,
- (bool)idPassForm.checkBox_AutoLogin.IsChecked
- );
-
- loginMethodInit();
- }
-
- private void ShowAccountListPage()
- {
- try
- {
- loginPage.qr.CloseEnlargeWindow();
- frame.Content = accountList;
- btn_Region.Visibility = Visibility.Collapsed;
-
- redrawSAccountList();
-
- if (!this.pingWorker.IsBusy)
- this.pingWorker.RunWorkerAsync();
-
- updateRemainPoint(this.bfClient.remainPoint);
-
- accountList.list_Account.Focus();
-
- bool hasAccounts = this.bfClient.accountList.Count() > 0;
- bool wantsAutoStart = (bool)settingPage.autoStartGame.IsChecked && hasAccounts;
- if (wantsAutoStart)
- {
- bool useTradLogin =
- (bool)settingPage.tradLogin.IsChecked && login_action_type == 1;
- if (useTradLogin || login_action_type == 0)
- runGame();
- accountList.btnGetOtp_Click(null, null);
- }
- }
- catch
- {
- errexit(TryFindResource("LoginNoAccountMatch") as string, 1);
- }
- }
-
- // Login do work.
- private void loginWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- //if (this.pingWorker.IsBusy) this.pingWorker.CancelAsync();
- // while (this.pingWorker.IsBusy)
- // Thread.Sleep(137);
- CancelWork();
-
- Console.WriteLine("loginWorker starting");
- Thread.CurrentThread.Name = "Login Worker";
- e.Result = "";
- try
- {
- loginWaitPage.Dispatcher.Invoke(
- new Action(
- delegate
- {
- if (App.LoginMethod != (int)LoginMethod.QRCode)
- this.bfClient = new BeanfunClient();
- this.bfClient.Login(
- loginPage.id_pass.t_AccountID.Text,
- loginPage.id_pass.t_Password.Password,
- App.LoginMethod,
- this.qrcodeClass,
- this.service_code,
- this.service_region
- );
- }
- )
- );
- if (this.bfClient.errmsg != null)
- e.Result = this.bfClient.errmsg;
- else
- e.Result = null;
- }
- catch (Exception ex)
- {
- e.Result =
- (TryFindResource("LoginErrorUnknown") as string)
- + "\n\n"
- + ex.Message
- + "\n"
- + ex.StackTrace;
- }
-
- ResumeWork();
- }
-
- // Login completed.
- private void loginWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- Console.WriteLine("loginWorker end");
- if (e != null && e.Error != null)
- {
- errexit(e.Error.Message, 1);
- NavigateLoginPage();
- return;
- }
- if (e != null && (string)e.Result != null)
- {
- if ((string)e.Result == "need_totp")
- {
- frame.Content = loginTotp;
- loginTotp.btn_login.IsEnabled = true;
- loginTotp.btn_cancel.IsEnabled = true;
- loginTotp.otp1.Text = "";
- loginTotp.otp2.Text = "";
- loginTotp.otp3.Text = "";
- loginTotp.otp4.Text = "";
- loginTotp.otp5.Text = "";
- loginTotp.otp6.Text = "";
- loginTotp.otp1.Focus();
- return;
- }
- else if ((string)e.Result == "LoginAdvanceCheck")
- {
- MessageBox.Show(TryFindResource("MsgNeedAuth") as string);
-
- // Handle panel switching.
- frame.Content = verifyPage;
- if ((bool)verifyPage.checkBoxRememberVerify.IsChecked)
- verifyPage.t_Code.Focus();
- else
- verifyPage.t_Verify.Focus();
- verifyPage.t_Verify.Text = accountManager.getVerifyByAccount(
- App.LoginRegion,
- loginPage.id_pass.t_AccountID.Text
- );
- verifyPage.checkBoxRememberVerify.IsChecked =
- verifyPage.t_Verify.Text != null && verifyPage.t_Verify.Text != "";
- verifyPage.t_Code.Text = "";
- string response = this.bfClient.getVerifyPageInfo();
- if (response == null)
- {
- MessageBox.Show(I18n.ToSimplified(this.bfClient.errmsg));
- NavigateLoginPage();
- }
- string errmsg = reLoadVerifyPage(response);
- if (errmsg != null)
- {
- MessageBox.Show(I18n.ToSimplified(errmsg));
- NavigateLoginPage();
- }
- }
- else if (((string)e.Result).StartsWith("bfAPPAutoLogin.ashx"))
- {
- string[] args = Regex.Split((string)e.Result, "\",\"");
- if (args.Length < 2)
- {
- errexit("LoginUnknown", 1);
- return;
- }
- loginWaitPage.t_Info.Content = Regex.Unescape(
- TryFindResource("MsgNeedBeanfunAuth") as string
- );
- bfAPPAutoLogin.IsEnabled = true;
- }
- else
- {
- errexit((string)e.Result, 1);
- }
- return;
- }
-
- OnLoginCompleted();
- }
-
- // totp do work.
- private void totpWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- //if (this.pingWorker.IsBusy) this.pingWorker.CancelAsync();
- // while (this.pingWorker.IsBusy)
- // Thread.Sleep(137);
- CancelWork();
-
- Console.WriteLine("loginWorker starting");
- Thread.CurrentThread.Name = "Totp Worker";
- e.Result = "";
- try
- {
- loginWaitPage.Dispatcher.Invoke(
- new Action(
- delegate
- {
- this.bfClient.TotpLogin(
- loginTotp.otp1.Text,
- loginTotp.otp2.Text,
- loginTotp.otp3.Text,
- loginTotp.otp4.Text,
- loginTotp.otp5.Text,
- loginTotp.otp6.Text,
- this.service_code,
- this.service_region
- );
- }
- )
- );
- if (this.bfClient.errmsg != null)
- e.Result = this.bfClient.errmsg;
- else
- e.Result = null;
- }
- catch (Exception ex)
- {
- e.Result =
- (TryFindResource("LoginErrorUnknown") as string)
- + "\n\n"
- + ex.Message
- + "\n"
- + ex.StackTrace;
- }
-
- ResumeWork();
- }
-
- // Login completed.
- private void totpWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- Console.WriteLine("loginWorker end");
- if (e != null && e.Error != null)
- {
- errexit(e.Error.Message, 1);
- NavigateLoginPage();
- return;
- }
- if (e != null && (string)e.Result != null)
- {
- if ((string)e.Result == "LoginAdvanceCheck")
- {
- MessageBox.Show(TryFindResource("MsgNeedAuth") as string);
-
- // Handle panel switching.
- frame.Content = verifyPage;
- if ((bool)verifyPage.checkBoxRememberVerify.IsChecked)
- verifyPage.t_Code.Focus();
- else
- verifyPage.t_Verify.Focus();
- verifyPage.t_Verify.Text = accountManager.getVerifyByAccount(
- App.LoginRegion,
- loginPage.id_pass.t_AccountID.Text
- );
- verifyPage.checkBoxRememberVerify.IsChecked =
- verifyPage.t_Verify.Text != null && verifyPage.t_Verify.Text != "";
- verifyPage.t_Code.Text = "";
- string response = this.bfClient.getVerifyPageInfo();
- if (response == null)
- {
- MessageBox.Show(I18n.ToSimplified(this.bfClient.errmsg));
- NavigateLoginPage();
- }
- string errmsg = reLoadVerifyPage(response);
- if (errmsg != null)
- {
- MessageBox.Show(I18n.ToSimplified(errmsg));
- NavigateLoginPage();
- }
- }
- else if (((string)e.Result).StartsWith("bfAPPAutoLogin.ashx"))
- {
- string[] args = Regex.Split((string)e.Result, "\",\"");
- if (args.Length < 2)
- {
- errexit("LoginUnknown", 1);
- return;
- }
- loginWaitPage.t_Info.Content = Regex.Unescape(
- TryFindResource("MsgNeedBeanfunAuth") as string
- );
- bfAPPAutoLogin.IsEnabled = true;
- }
- else
- {
- errexit((string)e.Result, 1);
- }
- return;
- }
-
- OnLoginCompleted();
- }
-
- private void redrawSAccountList()
- {
- if (this.bfClient.accountAmountLimitNotice != "")
- {
- accountList.lbl_AccountAmountLimitNotice.Content =
- this.bfClient.accountAmountLimitNotice;
- accountList.accLimit.Visibility = Visibility.Visible;
- int accLimit;
- try
- {
- accLimit = int.Parse(
- this.bfClient.accountAmountLimitNotice.Substring(
- this.bfClient.accountAmountLimitNotice.Length - 1,
- 1
- )
- );
- }
- catch
- {
- accLimit = -1;
- }
- if (accLimit == -1)
- {
- accountList.btnAddServiceAccount.Content = TryFindResource("GoToVerify");
- accountList.btnAddServiceAccount.IsEnabled = true;
- accountList.btnAddServiceAccount.Visibility = Visibility.Visible;
- }
- else
- {
- accountList.btnAddServiceAccount.Content = TryFindResource("AddServiceAccount");
- accountList.btnAddServiceAccount.IsEnabled =
- this.bfClient.accountList.Count < accLimit;
- accountList.btnAddServiceAccount.Visibility =
- this.bfClient.accountList.Count < accLimit
- ? Visibility.Visible
- : Visibility.Hidden;
- }
- }
- else
- {
- accountList.lbl_AccountAmountLimitNotice.Content = "";
- accountList.accLimit.Visibility = Visibility.Collapsed;
- }
-
- accountList.list_Account.ItemsSource = null;
- accountList.list_Account.ItemsSource = this.bfClient.accountList;
-
- string gameCode = service_code + "_" + service_region;
- Visibility visable =
- App.LoginRegion == "TW" ? Visibility.Visible : Visibility.Collapsed;
- if (accountList.list_Account.Items.Count > 0)
- {
- accountList.list_Account.SelectedIndex = 0;
-
- accountList.m_CopyAccount.Visibility = Visibility.Visible;
- //accountList.m_ChangeAccName.Visibility = !UnconnectedGame || App.LoginRegion != "TW" ? Visibility.Visible : Visibility.Collapsed;
- accountList.m_ChangePassword.Visibility = UnconnectedGame
- ? Visibility.Visible
- : Visibility.Collapsed;
- //accountList.m_AccInfo.Visibility = !UnconnectedGame || App.LoginRegion != "TW" ? Visibility.Visible : Visibility.Collapsed;
- accountList.s_Account.Visibility = Visibility.Visible;
- }
- else
- {
- accountList.m_CopyAccount.Visibility = Visibility.Collapsed;
- //accountList.m_ChangeAccName.Visibility = Visibility.Collapsed;
- accountList.m_ChangePassword.Visibility = Visibility.Collapsed;
- //accountList.m_AccInfo.Visibility = Visibility.Collapsed;
- accountList.s_Account.Visibility = Visibility.Collapsed;
- }
- accountList.m_GetEmail.Visibility = visable;
-
- if (gameCode == "610074_T9" || gameCode == "610075_T9" || gameCode == "610096_TE")
- accountList.btn_Tools.Visibility = Visibility.Visible;
- else
- accountList.btn_Tools.Visibility = Visibility.Collapsed;
- }
-
- public void updateRemainPoint(int remainPoint)
- {
- accountList.m_RemainPoint.Header = string.Format(
- TryFindResource("GashRemain") as string,
- $"{remainPoint}{(App.LoginRegion == "TW" || remainPoint == 0 ? "" : string.Format(TryFindResource("GashRemainInGame") as string, Math.Floor(remainPoint / 2.5)))}"
- );
- }
-
- public void runGame(string account = null, string password = null)
- {
- string gameCode = service_code + "_" + service_region;
- string gamePath = settingPage.t_GamePath.Text;
- if (gamePath == "" || !File.Exists(gamePath))
- {
- MessageBoxResult result = MessageBox.Show(
- TryFindResource("MsgCantFindGame") as string,
- "",
- MessageBoxButton.YesNo
- );
- if (result == MessageBoxResult.Yes || SelectedGame == null)
- {
- btn_SetGamePath_Click(null, null);
- }
- else
- {
- Process.Start(
- new ProcessStartInfo(SelectedGame.download_url) { UseShellExecute = true }
- );
- }
- return;
- }
- gamePath = settingPage.t_GamePath.Text;
- if (gamePath == "" || !File.Exists(gamePath))
- {
- return;
- }
-
- for (int i = 0; i < gamePath.Length; i++)
- {
- if (
- Convert.ToInt32(Convert.ToChar(gamePath.Substring(i, 1)))
- > Convert.ToInt32(Convert.ToChar(128))
- )
- {
- MessageBox.Show(TryFindResource("MsgGamePathHaveWChar") as string);
- break;
- }
- }
-
- List processIds = new List();
-
- Regex regexx = new Regex("(.*).exe");
- string gameProcessName = "";
- if (regexx.IsMatch(game_exe))
- gameProcessName = regexx.Match(game_exe).Groups[1].Value;
- if (gameProcessName != "")
- {
- foreach (Process process in Process.GetProcessesByName(gameProcessName))
- {
- if (processIds.Contains(process.Id))
- {
- continue;
- }
- try
- {
- using (
- ManagementObjectSearcher searcher = new ManagementObjectSearcher(
- "select * from Win32_Process where ProcessId = " + process.Id
- )
- )
- using (ManagementObjectCollection objects = searcher.Get())
- {
- if (
- gamePath
- == objects
- .Cast()
- .SingleOrDefault()
- ?["executablepath"]?.ToString()
- )
- {
- processIds.Add(process.Id);
- continue;
- }
- }
- }
- catch { }
- try
- {
- if (process.MainModule.FileName == gamePath)
- {
- processIds.Add(process.Id);
- continue;
- }
- }
- catch { }
- }
- }
-
- if (processIds.Count > 0)
- {
- MessageBoxResult result = MessageBox.Show(
- TryFindResource("MsgGameAlreadyRun") as string,
- "",
- MessageBoxButton.YesNo
- );
- if (result == MessageBoxResult.Yes)
- {
- foreach (int processId in processIds)
- {
- try
- {
- Process process = Process.GetProcessById(processId);
- process.Kill();
- }
- catch { }
- }
- }
- }
- try
- {
- Console.WriteLine("try open game");
- int runMode = int.Parse(ConfigAppSettings.GetValue("startGameMode", "0"));
- if (runMode == (int)GameStartMode.Auto)
- {
- switch (WindowsAPI.GetSystemDefaultLocaleName())
- {
- case "zh-Hant":
- case "zh-CHT":
- case "zh-TW":
- case "zh-HK":
- case "zh-MO":
- runMode = (int)GameStartMode.Normal;
- break;
- default:
- if (App.OSVersion < App.WinVista)
- {
- errexit(TryFindResource("MsgLEDoNotSupportXP") as string, 2);
- return;
- }
- else
- {
- runMode = (int)GameStartMode.LocaleRemulator;
- break;
- }
- }
- }
-
- if (runMode > (int)GameStartMode.LocaleRemulator)
- runMode = (int)GameStartMode.LocaleRemulator;
-
- string commandLine = "";
- if (
- account != null
- && password != null
- && account != ""
- && password != ""
- && game_commandLine != ""
- )
- {
- commandLine = game_commandLine;
- Regex regex = new Regex("%s");
- commandLine = regex.Replace(commandLine, account, 1);
- commandLine = regex.Replace(commandLine, password, 1);
- }
-
- switch (runMode)
- {
- case (int)GameStartMode.LocaleRemulator:
- startByLR(gamePath, commandLine);
- break;
- case (int)GameStartMode.Normal:
- ProcessStartInfo startInfo = new ProcessStartInfo(gamePath);
- startInfo.WorkingDirectory = Path.GetDirectoryName(gamePath);
- startInfo.Arguments = commandLine;
- Process.Start(startInfo);
- break;
- }
- Console.WriteLine("try open game done");
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.ToString());
- errexit(Regex.Unescape(TryFindResource("MsgLocalePluginRunError") as string), 2);
- }
- }
-
- private void startByLR(string path, string command)
- {
- if (
- App.ReleaseResource("LRConfig.xml") == -1
- || App.ReleaseResource("LRHookx32.dll") == -1
- || App.ReleaseResource("LRHookx64.dll") == -1
- || App.ReleaseResource("LRProc.exe") == -1
- || App.ReleaseResource("LRSubMenus.dll") == -1
- )
- {
- MessageBox.Show(TryFindResource("MsgLocalePluginReleaseError") as string);
- return;
- }
-
- var commandLine = string.Empty;
- commandLine = path.StartsWith("\"") ? $"{path} " : $"\"{path}\" ";
- commandLine += command;
- System.Globalization.TextInfo culInfo = System
- .Globalization.CultureInfo.GetCultureInfo("zh-HK")
- .TextInfo;
-
- new Thread(
- new ThreadStart(() =>
- {
- try
- {
- var proc = new Process();
- proc.StartInfo.FileName = Path.Combine(App.AppDir, "LRProc.exe");
- proc.StartInfo.Arguments =
- "ef3e7b42-a87c-4c07-ae3e-eeebeef12762 " + commandLine;
- proc.StartInfo.WorkingDirectory = Path.GetDirectoryName(path);
- proc.StartInfo.UseShellExecute = true;
- proc.StartInfo.Verb = "runas";
- proc.Start();
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.ToString());
- errexit(
- Regex.Unescape(TryFindResource("MsgLocalePluginRunError") as string),
- 2
- );
- }
- })
- ).Start();
- }
-
- public bool AddServiceAccount(string name)
- {
- if (this.bfClient == null)
- return false;
- if (name == null || name == "")
- return false;
-
- if (this.bfClient.AddServiceAccount(name, service_code, service_region))
- {
- this.bfClient.GetAccounts(service_code, service_region);
- int index = accountList.list_Account.SelectedIndex;
- redrawSAccountList();
- accountList.list_Account.SelectedIndex = index;
- return true;
- }
- return false;
- }
-
- public string UnconnectedGame_AddAccount(
- string name,
- string txtNewPwd,
- string txtNewPwd2,
- string txtServiceAccountDN,
- System.Collections.Specialized.NameValueCollection payload
- )
- {
- if (this.bfClient == null)
- return null;
- if (name == null || name == "")
- return null;
- if (txtNewPwd == null || txtNewPwd == "")
- return null;
- if (txtNewPwd2 == null || txtNewPwd2 == "")
- return null;
-
- string result = this.bfClient.UnconnectedGame_AddAccount(
- service_code,
- service_region,
- name,
- txtNewPwd,
- txtNewPwd2,
- txtServiceAccountDN,
- payload
- );
- if (result == "")
- {
- this.bfClient.GetAccounts(service_code, service_region);
- int index = accountList.list_Account.SelectedIndex;
- redrawSAccountList();
- accountList.list_Account.SelectedIndex = index;
- }
- return result;
- }
-
- public string UnconnectedGame_ChangePassword(string txtEmail)
- {
- if (this.bfClient == null)
- return null;
- if (txtEmail == null)
- return null;
-
- return this.bfClient.UnconnectedGame_ChangePassword(
- service_code,
- service_region,
- accountList.list_Account.SelectedIndex,
- txtEmail
- );
- }
-
- public System.Collections.Specialized.NameValueCollection UnconnectedGame_AddAccountInit()
- {
- if (this.bfClient == null)
- return null;
- return this.bfClient.UnconnectedGame_InitAddAccountPayload(
- service_code,
- service_region
- );
- }
-
- public System.Collections.Specialized.NameValueCollection UnconnectedGame_AddUnconnectedCheck(
- string name,
- string txtServiceAccountDN,
- System.Collections.Specialized.NameValueCollection payload
- )
- {
- if (this.bfClient == null)
- return null;
- return this.bfClient.UnconnectedGame_AddAccountCheck(
- service_code,
- service_region,
- name,
- txtServiceAccountDN,
- payload
- );
- }
-
- public System.Collections.Specialized.NameValueCollection UnconnectedGame_AddAccountCheckNickName(
- string txtServiceAccountDN,
- System.Collections.Specialized.NameValueCollection payload
- )
- {
- if (this.bfClient == null)
- return null;
- return this.bfClient.UnconnectedGame_AddAccountCheckNickName(
- service_code,
- service_region,
- txtServiceAccountDN,
- payload
- );
- }
-
- public bool ChangeServiceAccountDisplayName(string newName)
- {
- if (this.bfClient == null)
- return false;
- BeanfunClient.ServiceAccount account = (BeanfunClient.ServiceAccount)
- accountList.list_Account.SelectedItem;
- if (newName == null || newName == "" || account == null)
- return false;
- if (newName == account.sname)
- return true;
-
- string gameCode = service_code + "_" + service_region;
- if (this.bfClient.ChangeServiceAccountDisplayName(newName, gameCode, account))
- {
- account.sname = newName;
- int index = accountList.list_Account.SelectedIndex;
- redrawSAccountList();
- accountList.list_Account.SelectedIndex = index;
- return true;
- }
- return false;
- }
-
- public string GetServiceContract()
- {
- if (this.bfClient == null)
- return "";
-
- return this.bfClient.GetServiceContract(service_code, service_region);
- }
-
- // getOTP do work.
- private void getOtpWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- CancelWork();
-
- Console.WriteLine("getOtpWorker start");
- Thread.CurrentThread.Name = "GetOTP Worker";
- int index = (int)e.Argument;
- Console.WriteLine("Count = " + this.bfClient.accountList.Count + " | index = " + index);
- if (this.bfClient.accountList.Count <= index)
- {
- return;
- }
- Console.WriteLine("call GetOTP");
- Monitor.Enter(_bfClientLock);
- try
- {
- this.otp = this.bfClient.GetOTP(
- this.bfClient.accountList[index],
- this.service_code,
- this.service_region
- );
- }
- finally
- {
- Monitor.Exit(_bfClientLock);
- }
- Console.WriteLine("call GetOTP done");
- if (this.otp == null)
- e.Result = -1;
- else
- {
- e.Result = index;
- }
-
- ResumeWork();
- return;
- }
-
- // getOTP completed.
- private void getOtpWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- accountList.btnGetOtp.Content = TryFindResource("GetOtp") as string;
- if (e.Error != null)
- {
- errexit(e.Error.Message, 2, TryFindResource("GetOtpFailed") as string);
- }
- else
- {
- int index = (int)e.Result;
-
- if (index == -1)
- {
- errexit(this.bfClient.errmsg, 2, TryFindResource("GetOtpFailed") as string);
- }
- else
- {
- int accIndex = accountList.list_Account.SelectedIndex;
- string acc = this.bfClient.accountList[index].sid;
- accountList.t_Password.Text = this.otp;
-
- if (!(bool)settingPage.tradLogin.IsChecked && login_action_type == 1)
- {
- runGame(acc, accountList.t_Password.Text);
- }
- else
- {
- IntPtr hWnd = WindowsAPI.FindWindow(win_class_name, null);
- if ("MapleStoryClass".Equals(win_class_name) && hWnd == IntPtr.Zero)
- {
- hWnd = WindowsAPI.FindWindow("MapleStoryClassTW", null);
- }
- if (
- hWnd == IntPtr.Zero
- || !(bool)accountList.autoPaste.IsChecked
- || accountList.autoPaste.Visibility != Visibility.Visible
- )
- {
- try
- {
- WindowsAPI.CopyText(accountList.t_Password.Text);
- }
- catch { }
- ShowOtpCopiedHint();
- }
- else
- {
- System.Drawing.Size wndSize = System.Drawing.Size.Empty;
- if (hWnd != IntPtr.Zero)
- {
- wndSize = WindowsAPI.GetClientAreaSize(hWnd);
- }
-
- if (wndSize != System.Drawing.Size.Empty)
- {
- const int WM_KEYDOWN = 0X100;
- const int WM_LBUTTONDOWN = 0x0201;
- const byte VK_BACK = 0x0008;
- const byte VK_TAB = 0x0009;
- const byte VK_ENTER = 0x000D;
- const byte VK_ESCAPE = 0x001B;
- const byte VK_END = 0x0023;
- WindowsAPI.SetForegroundWindow(hWnd);
- Thread.Sleep(100);
- if ("610074".Equals(service_code) && "T9".Equals(service_region))
- {
- // 按下ESC關閉提示框
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_ESCAPE);
- Thread.Sleep(100);
- // 選中帳號欄
- System.Drawing.Point oldPoint = new System.Drawing.Point(0, 0);
- WindowsAPI.GetCursorPos(ref oldPoint);
- System.Drawing.Point point = new System.Drawing.Point(0, 0);
- WindowsAPI.ClientToScreen(hWnd, ref point);
- System.Drawing.Point textBoxPoint = new System.Drawing.Point(
- (int)(wndSize.Width * 0.5),
- (int)(wndSize.Height * 0.4)
- );
- WindowsAPI.SetCursorPos(
- point.X + textBoxPoint.X,
- point.Y + textBoxPoint.Y
- );
- int pos = (textBoxPoint.X & 0xFFFF) | (textBoxPoint.Y << 16);
- WindowsAPI.PostMessage(hWnd, WM_LBUTTONDOWN, 1, pos);
- Thread.Sleep(200);
- WindowsAPI.SetCursorPos(oldPoint.X, oldPoint.Y);
- }
- // 清空帳號欄內容
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_END);
- for (int i = 0; i < 64; i++)
- {
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_BACK);
- }
- // 輸入帳號
- WindowsAPI.PostString(hWnd, acc);
- // 切換到密碼欄
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_TAB);
- // 清空密碼欄內容
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_END);
- for (int i = 0; i < 20; i++)
- {
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_BACK);
- }
- // 輸入密碼
- WindowsAPI.PostString(hWnd, accountList.t_Password.Text);
- // 按登入
- WindowsAPI.PostKey(hWnd, WM_KEYDOWN, VK_ENTER);
- }
- }
- }
- }
- }
-
- Console.WriteLine("getOtpWorker end");
-
- accountList.list_Account.IsEnabled = true;
- accountList.btnGetOtp.IsEnabled = true;
- accountList.btn_Logout.IsEnabled = true;
- accountList.btn_ChangeGame.IsEnabled = true;
- accountList.gameName.IsEnabled = true;
- accountList.btn_StartGame.IsEnabled = true;
- accountList.m_MenuList.IsEnabled = true;
- if (this.bfClient.accountAmountLimitNotice != "")
- accountList.btnAddServiceAccount.IsEnabled =
- this.bfClient.accountList.Count
- < int.Parse(
- this.bfClient.accountAmountLimitNotice.Substring(
- this.bfClient.accountAmountLimitNotice.Length - 1,
- 1
- )
- );
- }
-
- public void ShowOtpCopiedHint(string message = null)
- {
- accountList.toastText.Text =
- "✓ " + (message ?? TryFindResource("GetOtpSuccessAndCopy") as string ?? "Copied!");
- accountList.toastBorder.Background = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("#CC2E7D32")
- );
- accountList.toastBorder.Visibility = Visibility.Visible;
- var timer = new System.Windows.Threading.DispatcherTimer
- {
- Interval = TimeSpan.FromSeconds(2),
- };
- timer.Tick += (s, _) =>
- {
- timer.Stop();
- accountList.toastBorder.Visibility = Visibility.Collapsed;
- accountList.toastBorder.Background = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString("#CC333333")
- );
- };
- timer.Start();
- }
-
- // Ping to Beanfun website.
- private void pingWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- Thread.CurrentThread.Name = "ping Worker";
- Console.WriteLine("pingWorker start");
- const int WaitSecs = 60; // 1min
-
- while (!isCancelRequested)
- {
- if (this.pingWorker.CancellationPending)
- {
- Console.WriteLine("break duo to cancel");
- break;
- }
-
- if (
- this.getOtpWorker.IsBusy
- || this.loginWorker.IsBusy
- || this.totpWorker.IsBusy
- || this.qrWorker.IsBusy
- || this.verifyWorker.IsBusy
- )
- {
- Console.WriteLine("ping.busy sleep 1s");
- System.Threading.Thread.Sleep(1000 * 1);
- continue;
- }
-
- if (this.bfClient != null && Monitor.TryEnter(_bfClientLock))
- {
- try
- {
- this.bfClient.Ping();
- }
- finally
- {
- Monitor.Exit(_bfClientLock);
- }
- }
-
- for (int i = 0; i < WaitSecs; ++i)
- {
- if (this.pingWorker.CancellationPending)
- break;
- System.Threading.Thread.Sleep(1000 * 1);
- }
- }
- }
-
- private void pingWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- Console.WriteLine("ping.done");
- }
-
- private void qrWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- if (qrWorker.CancellationPending)
- {
- e.Cancel = true;
- return;
- }
- this.bfClient = new BeanfunClient();
- string skey = this.bfClient.GetSessionkey();
- this.qrcodeClass = this.bfClient.GetQRCodeValue(skey);
- }
-
- private void qrWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- btn_Region.IsEnabled = true;
- if (e.Cancelled)
- return;
- if (updateQRCodeImage())
- {
- qrCheckLogin.IsEnabled = true;
- }
- else
- {
- App.LoginMethod = (int)LoginMethod.Regular;
- Dispatcher.BeginInvoke(new System.Action(() => loginMethodChanged()));
- }
- }
-
- private void qrCheckLogin_Tick(object sender, EventArgs e)
- {
- if (this.qrcodeClass == null)
- {
- MessageBox.Show("QRCode not get yet");
- return;
- }
- if (!Monitor.TryEnter(_bfClientLock))
- return;
- int res;
- try
- {
- res = this.bfClient.QRCodeCheckLoginStatus(this.qrcodeClass);
- }
- finally
- {
- Monitor.Exit(_bfClientLock);
- }
- if (res != 0)
- this.qrCheckLogin.IsEnabled = false;
- if (res == 1)
- {
- do_Login();
- }
- if (res == -2)
- {
- refreshQRCode();
- }
- }
-
- public void refreshQRCode()
- {
- if (!qrWorker.IsBusy)
- qrWorker.RunWorkerAsync(loginPage == null || loginPage.qr == null ? false : true);
- }
-
- public bool updateQRCodeImage()
- {
- loginPage.qr.btn_Refresh_QRCode.IsEnabled = false;
-
- BitmapImage qrCodeImage;
- bool result;
- if (
- this.qrcodeClass == null
- || (qrCodeImage = this.bfClient.getQRCodeImage(qrcodeClass)) == null
- )
- {
- result = false;
- loginPage.qr.qr_image.Source = qr_default;
- loginPage.qr.btn_CopyQR.IsEnabled = false;
- loginPage.qr.btn_EnlargeQR.IsEnabled = false;
- }
- else
- {
- result = true;
- loginPage.qr.qr_image.Source = qrCodeImage;
- loginPage.qr.btn_CopyQR.IsEnabled = true;
- loginPage.qr.btn_EnlargeQR.IsEnabled = true;
- }
- loginPage.qr.btn_Refresh_QRCode.IsEnabled = true;
-
- return result;
- }
-
- private void bfAPPAutoLogin_Tick(object sender, EventArgs e)
- {
- if (!Monitor.TryEnter(_bfClientLock))
- return;
- JObject resultJson;
- try
- {
- resultJson = this.bfClient.CheckIsRegisteDevice(service_code, service_region);
- }
- finally
- {
- Monitor.Exit(_bfClientLock);
- }
- if (resultJson == null || resultJson["IntResult"] == null)
- return;
- if ((string)resultJson["IntResult"] != "1" && (string)resultJson["IntResult"] != "0")
- this.bfAPPAutoLogin.IsEnabled = false;
-
- switch ((string)resultJson["IntResult"])
- {
- case "-3":
- Console.WriteLine("登入請求被拒絕");
- errexit(TryFindResource("MsgBeanfunRejectLogin") as string, 1);
- break;
- case "-2":
- Console.WriteLine("登入請求已逾時");
- NavigateLoginPage();
- break;
- case "-1":
- errexit((string)resultJson["StrReslut"], 1);
- break;
- case "0":
- return;
- case "1":
- Console.WriteLine("尚未授權本次登入");
- return;
- case "2":
- loginWorker_RunWorkerCompleted(null, null);
- break;
- }
- loginWaitPage.t_Info.Content = TryFindResource("MsgLogging") as string;
- }
-
- private void checkPlayPage_Tick(object sender, EventArgs e)
- {
- try
- {
- const uint WM_CLOSE = 0x10;
- IntPtr hWnd;
- if ((hWnd = WindowsAPI.FindWindow("StartUpDlgClass", "MapleStory")) != IntPtr.Zero)
- WindowsAPI.PostMessage(hWnd, WM_CLOSE, 0, 0);
- }
- catch { }
- }
-
- private void checkPatcher_Tick(object sender, EventArgs e)
- {
- if (settingPage == null || settingPage.t_GamePath == null)
- return;
- bool found = false;
- try
- {
- string patherPath =
- Path.GetDirectoryName(settingPage.t_GamePath.Text) + "\\Patcher.exe";
- foreach (Process process in Process.GetProcessesByName("Patcher"))
- {
- try
- {
- if (process.MainModule.FileName == patherPath)
- {
- process.Kill();
- found = true;
- }
- }
- catch { }
- }
- }
- catch { }
-
- if (found)
- {
- short ClientMapleMajor = 0;
- short ClientMapleMinor = 0;
- short SrvMapleMajor = 0;
- string SrvMapleMinor = "";
- try
- {
- // 獲取客戶端版本
- FileVersionInfo fileVerInfo = FileVersionInfo.GetVersionInfo(
- settingPage.t_GamePath.Text
- );
- ClientMapleMajor = (short)fileVerInfo.ProductMinorPart;
- ClientMapleMinor = (short)fileVerInfo.FileBuildPart;
-
- // 獲取伺服器版本
- CancellationTokenSource c = new CancellationTokenSource();
- CancellationToken token = c.Token;
- byte[] Data = null;
- System.Threading.Tasks.Task task = new System.Threading.Tasks.Task(
- () =>
- {
- try
- {
- var tcpClient = new System.Net.Sockets.TcpClient();
- tcpClient.SendTimeout = 6000;
- tcpClient.ReceiveTimeout = 6000;
- string WvsLoginServerDomain = "tw.login.maplestory.beanfun.com";
- if ("610075_T9".Equals(service_code + "_" + service_region))
- WvsLoginServerDomain = "tw.loginT.maplestory.beanfun.com";
- tcpClient.Connect(WvsLoginServerDomain, 8484);
-
- if (tcpClient.Connected)
- {
- Data = new Byte[1024];
- System.Net.Sockets.NetworkStream nsData = tcpClient.GetStream();
- Int32 bytes = nsData.Read(Data, 0, Data.Length);
- }
-
- tcpClient.Close();
- while (true)
- {
- if (token.IsCancellationRequested)
- {
- throw new OperationCanceledException();
- }
- }
- }
- catch { }
- ;
- },
- token
- );
-
- task.Start();
- task.Wait(3000, token);
- c.Cancel();
- if (Data != null)
- {
- if (accountList != null)
- {
- MapleLib.PacketLib.PacketReader packet =
- new MapleLib.PacketLib.PacketReader(Data);
- packet.ReadShort();
- SrvMapleMajor = packet.ReadShort();
- SrvMapleMinor = packet.ReadMapleString();
- packet.Skip(4);
- packet.Skip(4);
- byte MapleRegion = packet.ReadByte();
- Console.WriteLine(
- "伺服器版本: "
- + SrvMapleMajor
- + "\r\n小版本: "
- + SrvMapleMinor.Split(':')[0]
- + "\r\n區域號: "
- + MapleRegion
- );
-
- if (
- SrvMapleMajor == 0
- || SrvMapleMinor.Split(':')[0] == ""
- || MapleRegion == 0
- )
- Data = null;
- }
- }
- if (Data == null)
- {
- SrvMapleMajor = 0;
- SrvMapleMinor = "";
- }
- }
- catch { }
- string info = "";
- if (ClientMapleMajor != 0)
- {
- info +=
- $"\r\n{TryFindResource("ClientVersion") as string}{ClientMapleMajor}.{ClientMapleMinor}";
- if (SrvMapleMajor != 0 && SrvMapleMinor.Split(':')[0] != "")
- {
- info +=
- $"\r\n{TryFindResource("ServerVersion") as string}{SrvMapleMajor}.{SrvMapleMinor.Split(':')[0]}";
- }
- }
- bool isCanUpdate =
- ClientMapleMajor != 0
- && SrvMapleMajor != 0
- && ClientMapleMajor >= (SrvMapleMajor - 2);
- MessageBoxResult result = MessageBox.Show(
- string.Format(
- Regex.Unescape(TryFindResource("MsgKillPatcher") as string),
- info,
- isCanUpdate && ClientMapleMajor == SrvMapleMajor
- ? $"V{SrvMapleMajor}.{SrvMapleMinor.Split(':')[0]}fix"
- : "",
- isCanUpdate
- ? TryFindResource("UpdateByPatch")
- : TryFindResource("UpdateByFullClient"),
- isCanUpdate
- ? TryFindResource("GamePatch")
- : TryFindResource("GameFullClient")
- ),
- TryFindResource("WarningByBeanfun") as string,
- MessageBoxButton.YesNo
- );
- if (result == MessageBoxResult.Yes)
- Process.Start(
- new ProcessStartInfo(
- $"https://maplestory.beanfun.com/download{(isCanUpdate ? "?download_type=2" : "")}"
- )
- {
- UseShellExecute = true,
- }
- );
- }
- }
-
- private void verifyWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- e.Result = null;
- string response = "";
- verifyPage.Dispatcher.Invoke(
- new Action(
- delegate
- {
- response = this.bfClient.verify(
- viewstate,
- eventvalidation,
- samplecaptcha,
- verifyPage.t_Verify.Text,
- verifyPage.t_Code.Text
- );
- }
- )
- );
- Regex regex = new Regex("alert\\('(.*)'\\);");
- string msg = null;
- if (regex.IsMatch(response))
- {
- msg = regex.Match(response).Groups[1].Value;
- }
- if (msg == null)
- {
- if (response.Contains("圖形驗證碼輸入錯誤"))
- {
- MessageBox.Show(TryFindResource("WrongCaptcha") as string);
- }
- else
- {
- MessageBox.Show(TryFindResource("WrongAuthInfo") as string);
- }
- }
- else
- {
- if (msg.Contains("資料已驗證成功"))
- {
- e.Result = true;
- }
- else
- {
- MessageBox.Show(msg.Replace("\\n", "\n").Replace("\\r", "\r"));
- }
- }
- if (e.Result == null)
- {
- string errmsg = "Error Load Verify Page";
- verifyPage.Dispatcher.Invoke(
- new Action(
- delegate
- {
- errmsg = reLoadVerifyPage(response);
- verifyPage.t_Code.Text = "";
- }
- )
- );
- if (errmsg != null)
- {
- MessageBox.Show(I18n.ToSimplified(errmsg));
- }
- }
- }
-
- private void verifyWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if (e.Result != null)
- {
- do_Login();
- }
- }
- }
-}
diff --git a/Beanfun/Pages/About.xaml b/Beanfun/Pages/About.xaml
deleted file mode 100644
index 01156eb..0000000
--- a/Beanfun/Pages/About.xaml
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Github
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/About.xaml.cs b/Beanfun/Pages/About.xaml.cs
deleted file mode 100644
index 0e32415..0000000
--- a/Beanfun/Pages/About.xaml.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media;
-
-namespace Beanfun
-{
- ///
- /// About.xaml 的交互逻辑
- ///
- public partial class About : Page
- {
- public About()
- {
- InitializeComponent();
- version.Text = App.AssemblyVersion;
- initThemeColor(App.MainWnd.isLightColor());
- }
-
- public void initThemeColor(bool isLightMode)
- {
- Brush brush = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString(isLightMode ? "Black" : "White")
- );
- t_AppName.Foreground = brush;
- t_Author.Foreground = brush;
- t_Version.Foreground = brush;
- version.Foreground = brush;
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (App.MainWnd == null)
- return;
- if (App.MainWnd.return_page == null || App.MainWnd.return_page == App.MainWnd.loginPage)
- App.MainWnd.NavigateLoginPage();
- else
- App.MainWnd.frame.Content = App.MainWnd.return_page;
- App.MainWnd.return_page = null;
- }
-
- private void UpdateCheck_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.CheckUpdates(true);
- }
-
- private void MailContact_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- string to = "pungin@msn.com";
- string subject = Uri.EscapeDataString(
- TryFindResource("Feedback") as string ?? "Feedback"
- );
- string body = Uri.EscapeDataString(
- string.Format(TryFindResource("FeedbackText") as string ?? "{0}", version.Text)
- );
-
- string mailtoUrl = $"mailto:{to}?subject={subject}&body={body}";
-
- Process.Start(
- new ProcessStartInfo { FileName = mailtoUrl, UseShellExecute = true }
- );
- }
- catch (Exception ex)
- {
- Debug.WriteLine("Failed to open mail: " + ex.Message);
- }
- }
-
- private void Github_Click(object sender, RoutedEventArgs e)
- {
- // Fix for .NET 8
- System.Diagnostics.Process.Start(
- new System.Diagnostics.ProcessStartInfo
- {
- FileName = "https://github.com/pungin/Beanfun/issues/new",
- UseShellExecute = true,
- }
- );
- }
- }
-}
diff --git a/Beanfun/Pages/AccountList.xaml b/Beanfun/Pages/AccountList.xaml
deleted file mode 100644
index d889f75..0000000
--- a/Beanfun/Pages/AccountList.xaml
+++ /dev/null
@@ -1,320 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/AccountList.xaml.cs b/Beanfun/Pages/AccountList.xaml.cs
deleted file mode 100644
index 9f64383..0000000
--- a/Beanfun/Pages/AccountList.xaml.cs
+++ /dev/null
@@ -1,536 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
-
-namespace Beanfun
-{
- ///
- /// AccountList.xaml 的交互逻辑
- ///
- public partial class AccountList : Page
- {
- // drag and drop related variables
- private Point _dragStartPoint;
- private bool _isHandlePressed = false;
- private ListBoxItem _draggedItem = null;
- private int _draggedIndex = -1;
-
- public AccountList()
- {
- InitializeComponent();
-
- autoPaste.IsChecked = bool.Parse(ConfigAppSettings.GetValue("autoPaste", "false"));
- }
-
- private void btn_Logout_Click(object sender, RoutedEventArgs e)
- {
- MessageBoxResult result = MessageBox.Show(
- TryFindResource("LogoutConfirm") as string,
- TryFindResource("Logout") as string,
- MessageBoxButton.YesNo
- );
- if (result == MessageBoxResult.No)
- return;
- if (App.LoginMethod == (int)LoginMethod.QRCode)
- App.MainWnd.loginMethodChanged();
- App.MainWnd.NavigateLoginPage();
- }
-
- private void list_Account_MouseDoubleClick(object sender, RoutedEventArgs e)
- {
- BeanfunClient.ServiceAccount account = (BeanfunClient.ServiceAccount)
- list_Account.SelectedItem;
- if (account == null)
- return;
- try
- {
- WindowsAPI.CopyText(account.sid);
- }
- catch { }
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (
- (
- (bool)App.MainWnd.settingPage.tradLogin.IsChecked
- && App.MainWnd.login_action_type == 1
- )
- || App.MainWnd.login_action_type == 0
- )
- {
- btn_StartGame.IsEnabled = false;
- App.MainWnd.runGame();
- btn_StartGame.IsEnabled = true;
- }
- else
- btnGetOtp_Click(null, null);
- }
-
- private void autoPaste_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (bool.Parse(ConfigAppSettings.GetValue("autoPaste", "false")) == autoPaste.IsChecked)
- return;
- if (ConfigAppSettings.GetValue("autoPaste", "") == "")
- MessageBox.Show(TryFindResource("AutoPasteTip") as string);
- ConfigAppSettings.SetValue("autoPaste", Convert.ToString(autoPaste.IsChecked));
- }
-
- public void btnGetOtp_Click(object sender, RoutedEventArgs e)
- {
- if (list_Account.SelectedIndex < 0 || App.MainWnd.loginWorker.IsBusy)
- {
- MessageBox.Show(TryFindResource("MsgSelectAccount") as string);
- return;
- }
-
- this.btnGetOtp.Content = TryFindResource("GettingOtp") as string;
- this.t_Password.Text = "";
- this.list_Account.IsEnabled = false;
- this.btnGetOtp.IsEnabled = false;
- this.btn_Logout.IsEnabled = false;
- this.btn_ChangeGame.IsEnabled = false;
- this.gameName.IsEnabled = false;
- this.btn_StartGame.IsEnabled = false;
- this.btnAddServiceAccount.IsEnabled = false;
- this.m_MenuList.IsEnabled = false;
- App.MainWnd.getOtpWorker.RunWorkerAsync(list_Account.SelectedIndex);
- }
-
- private void t_Password_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (
- t_Password.Text == ""
- || (string)btnGetOtp.Content == TryFindResource("GettingOtp") as string
- )
- return;
- try
- {
- WindowsAPI.CopyText(t_Password.Text);
- }
- catch { }
- App.MainWnd.ShowOtpCopiedHint(TryFindResource("CopyFinished") as string ?? "Copied");
- }
-
- private void btnAddServiceAccount_Click(object sender, RoutedEventArgs e)
- {
- if (!btnAddServiceAccount.IsEnabled)
- return;
- if ((string)btnAddServiceAccount.Content == TryFindResource("GoToVerify") as string)
- {
- new WebBrowser("https://tw.beanfun.com/TW/member/verify_index.aspx").Show();
- }
- else
- {
- if (
- App.MainWnd.service_code == "610153" && App.MainWnd.service_region == "TN"
- || App.MainWnd.service_code == "610085" && App.MainWnd.service_region == "TC"
- )
- new UnconnectedGame_AddAccount().ShowDialog();
- else
- new AddServiceAccount().ShowDialog();
- }
- }
-
- private void m_UpdatePoint_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.updateRemainPoint(App.MainWnd.bfClient.getRemainPoint());
- }
-
- private void m_ChangeAccName_Click(object sender, RoutedEventArgs e)
- {
- BeanfunClient.ServiceAccount account = (BeanfunClient.ServiceAccount)
- list_Account.SelectedItem;
- if (account == null)
- return;
- new ChangeServiceAccountDisplayName(account.sname).ShowDialog();
- }
-
- private void bfb_Gash_Click(object sender, RoutedEventArgs e)
- {
- string url;
- if (App.LoginRegion == "TW")
- {
- url = "https://tw.beanfun.com/TW/";
- }
- else
- {
- url = "https://bfweb.hk.beanfun.com/HK/";
- }
- url +=
- $"auth.aspx?channel=gash&page_and_query=default.aspx%3Fservice_code%3D999999%26service_region%3DT0&web_token={App.MainWnd.bfClient.WebToken}";
- new WebBrowser(url).Show();
- }
-
- private void BF_btnMember_Click(object sender, RoutedEventArgs e)
- {
- string baseUrl;
- string pageQuery;
-
- if (App.LoginRegion == "TW")
- {
- baseUrl = "https://tw.beanfun.com/TW/";
- pageQuery = "index_new.aspx";
- }
- else
- {
- baseUrl = "https://bfweb.hk.beanfun.com/HK/";
- pageQuery = "default.aspx%3Fservice_code%3D999999%26service_region%3DT0";
- }
-
- string url =
- baseUrl
- + $"auth.aspx?channel=member&page_and_query={pageQuery}&web_token={App.MainWnd.bfClient.WebToken}";
-
- new WebBrowser(url).Show();
- }
-
- private void btn_Customerservice_Click(object sender, RoutedEventArgs e)
- {
- string url;
- if (App.LoginRegion == "TW")
- {
- url = "https://tw.beanfun.com/customerservice/www/main.aspx";
- }
- else
- {
- url = "https://bfweb.hk.beanfun.com/newfaq/service_newBF.aspx";
- }
- new WebBrowser(url).Show();
- }
-
- private void m_GetEmail_Click(object sender, RoutedEventArgs e)
- {
- new CopyBox(
- TryFindResource("AuthEmail") as string,
- App.MainWnd.bfClient.getEmail()
- ).ShowDialog();
- }
-
- private void m_AccInfo_Click(object sender, RoutedEventArgs e)
- {
- BeanfunClient.ServiceAccount account = (BeanfunClient.ServiceAccount)
- list_Account.SelectedItem;
- if (account == null)
- return;
- new ServiceAccountInfo(account).ShowDialog();
- }
-
- private void btn_HomePage_Click(object sender, RoutedEventArgs e)
- {
- if (App.MainWnd != null && App.MainWnd.SelectedGame != null)
- new WebBrowser(App.MainWnd.SelectedGame.website_url).Show();
- }
-
- private void gameName_Click(object sender, RoutedEventArgs e)
- {
- new GameList().ShowDialog();
- }
-
- private void m_ChangePassword_Click(object sender, RoutedEventArgs e)
- {
- new UnconnectedGame_ChangePassword().ShowDialog();
- }
-
- public void btn_Tools_Click(object sender, RoutedEventArgs e)
- {
- string gameCode = App.MainWnd.service_code + "_" + App.MainWnd.service_region;
- switch (gameCode)
- {
- case "610074_T9":
- case "610075_T9":
- new MapleTools().Show();
- break;
- case "610096_TE":
- new KartTools().Show();
- break;
- }
- }
-
- private void btn_Deposite_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://m.beanfun.com/Deposite").Show();
- }
-
- #region Drag and Drop Reorder
-
- private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- _dragStartPoint = e.GetPosition(null);
- _isHandlePressed = false;
-
- // get the clicked ListBoxItem
- var listBox = sender as ListBox;
- var item = FindAncestor((DependencyObject)e.OriginalSource);
-
- if (item != null)
- {
- _draggedItem = item;
- _draggedIndex = listBox.ItemContainerGenerator.IndexFromContainer(item);
- }
- }
-
- private void DragHandle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- _isHandlePressed = true;
- _dragStartPoint = e.GetPosition(null);
-
- var element = sender as FrameworkElement;
- var item = FindAncestor(element);
-
- if (item != null)
- {
- _draggedItem = item;
- _draggedIndex = list_Account.ItemContainerGenerator.IndexFromContainer(item);
- }
- }
-
- private void ListBox_PreviewMouseMove(object sender, MouseEventArgs e)
- {
- if (
- e.LeftButton != MouseButtonState.Pressed
- || _draggedItem == null
- || !_isHandlePressed
- )
- return;
-
- Point currentPosition = e.GetPosition(null);
- Vector diff = _dragStartPoint - currentPosition;
-
- // check if the distance is enough to start dragging
- if (
- Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance
- || Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance
- )
- {
- var listBox = sender as ListBox;
- var data = listBox.ItemContainerGenerator.ItemFromContainer(_draggedItem);
-
- if (data != null)
- {
- DataObject dragData = new DataObject("ServiceAccount", data);
- DragDrop.DoDragDrop(_draggedItem, dragData, DragDropEffects.Move);
- }
-
- _isHandlePressed = false;
- _draggedItem = null;
- }
- }
-
- private void ListBox_DragOver(object sender, DragEventArgs e)
- {
- if (!e.Data.GetDataPresent("ServiceAccount"))
- {
- e.Effects = DragDropEffects.None;
- return;
- }
-
- e.Effects = DragDropEffects.Move;
-
- var listBox = sender as ListBox;
- Point position = e.GetPosition(listBox);
-
- var targetItem = GetItemAtPosition(listBox, position);
- bool isLowerHalf = false;
-
- if (targetItem != null)
- {
- Point itemPosition = e.GetPosition(targetItem);
- isLowerHalf = itemPosition.Y > targetItem.ActualHeight / 2;
- }
-
- for (int i = 0; i < listBox.Items.Count; i++)
- {
- var container = listBox.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
- if (container != null)
- {
- if (container == targetItem && container != _draggedItem)
- {
- container.BorderBrush = new SolidColorBrush(Colors.DodgerBlue);
- // Show border on top or bottom based on cursor position
- container.BorderThickness = isLowerHalf
- ? new Thickness(0, 0, 0, 2)
- : new Thickness(0, 2, 0, 0);
- }
- else
- {
- container.BorderBrush = null;
- container.BorderThickness = new Thickness(0);
- }
- }
- }
- }
-
- private void ListBox_Drop(object sender, DragEventArgs e)
- {
- if (!e.Data.GetDataPresent("ServiceAccount"))
- return;
-
- var listBox = sender as ListBox;
- var droppedData = e.Data.GetData("ServiceAccount") as BeanfunClient.ServiceAccount;
-
- if (droppedData == null)
- return;
-
- Point position = e.GetPosition(listBox);
- var targetItem = GetItemAtPosition(listBox, position);
-
- int targetIndex = -1;
- bool insertAfter = false;
-
- if (targetItem != null)
- {
- targetIndex = listBox.ItemContainerGenerator.IndexFromContainer(targetItem);
-
- // Check if dropping on the lower half of the item
- Point itemPosition = e.GetPosition(targetItem);
- if (itemPosition.Y > targetItem.ActualHeight / 2)
- {
- insertAfter = true;
- }
- }
- else
- {
- // if there is no target item, put it at the end
- targetIndex = listBox.Items.Count;
- insertAfter = false;
- }
-
- for (int i = 0; i < listBox.Items.Count; i++)
- {
- var container = listBox.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem;
- if (container != null)
- {
- container.BorderBrush = null;
- container.BorderThickness = new Thickness(0);
- }
- }
-
- // perform reordering
- if (_draggedIndex != -1 && targetIndex != -1)
- {
- int finalIndex = targetIndex;
- if (insertAfter)
- finalIndex++;
-
- // Skip if dropping at the same position
- if (_draggedIndex == finalIndex || _draggedIndex == finalIndex - 1 && insertAfter)
- {
- _draggedItem = null;
- _draggedIndex = -1;
- return;
- }
-
- var accountList = App.MainWnd.bfClient.accountList;
- var item = accountList[_draggedIndex];
- accountList.RemoveAt(_draggedIndex);
-
- // Adjust index after removal
- if (_draggedIndex < finalIndex)
- finalIndex--;
-
- // Clamp to valid range
- if (finalIndex > accountList.Count)
- finalIndex = accountList.Count;
- if (finalIndex < 0)
- finalIndex = 0;
-
- accountList.Insert(finalIndex, item);
-
- listBox.ItemsSource = null;
- listBox.ItemsSource = accountList;
- listBox.SelectedIndex = finalIndex;
-
- SaveAccountOrder();
- }
-
- _draggedItem = null;
- _draggedIndex = -1;
- }
-
- private ListBoxItem GetItemAtPosition(ListBox listBox, Point position)
- {
- HitTestResult hitTestResult = VisualTreeHelper.HitTest(listBox, position);
- if (hitTestResult == null)
- return null;
-
- return FindAncestor(hitTestResult.VisualHit);
- }
-
- private static T FindAncestor(DependencyObject current)
- where T : DependencyObject
- {
- while (current != null)
- {
- if (current is T)
- return (T)current;
- current = VisualTreeHelper.GetParent(current);
- }
- return null;
- }
-
- ///
- /// Save the current game's account sorting order
- ///
- private void SaveAccountOrder()
- {
- if (App.MainWnd?.bfClient?.accountList == null)
- return;
-
- string gameCode = App.MainWnd.service_code + "_" + App.MainWnd.service_region;
- var accountList = App.MainWnd.bfClient.accountList;
-
- string orderString = string.Join(",", accountList.ConvertAll(a => a.sid));
- ConfigAppSettings.SetValue("AccountOrder_" + gameCode, orderString);
- }
-
- ///
- /// Reorder the account list based on the saved order
- ///
- public static void ApplyAccountOrder(
- List accountList,
- string gameCode
- )
- {
- string orderString = ConfigAppSettings.GetValue("AccountOrder_" + gameCode, "");
- if (string.IsNullOrEmpty(orderString))
- return;
-
- string[] orderArray = orderString.Split(',');
-
- // create a dictionary to quickly find the account
- var accountDict = new Dictionary();
- foreach (var account in accountList)
- {
- if (!accountDict.ContainsKey(account.sid))
- accountDict[account.sid] = account;
- }
-
- // reorder the account list based on the saved order
- var orderedList = new List();
- foreach (string sid in orderArray)
- {
- if (accountDict.ContainsKey(sid))
- {
- orderedList.Add(accountDict[sid]);
- accountDict.Remove(sid);
- }
- }
-
- // add the accounts that are not in the order to the end
- foreach (var account in accountDict.Values)
- {
- orderedList.Add(account);
- }
-
- // update the original list
- accountList.Clear();
- accountList.AddRange(orderedList);
- }
-
- #endregion
- }
-}
diff --git a/Beanfun/Pages/LoginPage.xaml b/Beanfun/Pages/LoginPage.xaml
deleted file mode 100644
index c67d1bf..0000000
--- a/Beanfun/Pages/LoginPage.xaml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/Beanfun/Pages/LoginPage.xaml.cs b/Beanfun/Pages/LoginPage.xaml.cs
deleted file mode 100644
index 28807fe..0000000
--- a/Beanfun/Pages/LoginPage.xaml.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Collections.Generic;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// LoginPage.xaml 的交互逻辑
- ///
- public partial class LoginPage : Page
- {
- public id_pass_form id_pass = new id_pass_form();
- public qr_form qr = new qr_form();
- public gamepass_form gamepass = new gamepass_form();
-
- public LoginPage()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/Beanfun/Pages/LoginTotp.xaml b/Beanfun/Pages/LoginTotp.xaml
deleted file mode 100644
index f4697c6..0000000
--- a/Beanfun/Pages/LoginTotp.xaml
+++ /dev/null
@@ -1,189 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/LoginTotp.xaml.cs b/Beanfun/Pages/LoginTotp.xaml.cs
deleted file mode 100644
index 5fd2134..0000000
--- a/Beanfun/Pages/LoginTotp.xaml.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// LoginTotp.xaml 的交互逻辑
- ///
- public partial class LoginTotp : Page
- {
- private TextBox[] _otpBoxes;
- private bool _isPasting;
-
- public LoginTotp()
- {
- InitializeComponent();
- _otpBoxes = new[] { otp1, otp2, otp3, otp4, otp5, otp6 };
- }
-
- private void btn_login_Click(object sender, RoutedEventArgs e)
- {
- btn_login.IsEnabled = false;
- btn_cancel.IsEnabled = false;
- App.MainWnd.do_Totp();
- }
-
- private void btn_back_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.frame.Content = App.MainWnd.loginPage;
- }
-
- private void otp_PreviewKeyDown(object sender, KeyEventArgs e)
- {
- var box = sender as TextBox;
- int index = System.Array.IndexOf(_otpBoxes, box);
-
- if (e.Key == Key.Back && box.Text.Length == 0 && index > 0)
- {
- _otpBoxes[index - 1].Text = "";
- _otpBoxes[index - 1].Focus();
- e.Handled = true;
- return;
- }
-
- if (
- e.Key == Key.V
- && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control
- )
- {
- e.Handled = true;
- HandlePaste();
- }
- }
-
- private void HandlePaste()
- {
- string text = "";
- try
- {
- text = Clipboard.GetText();
- }
- catch
- {
- return;
- }
- string digits = new string(text.Where(char.IsDigit).ToArray());
- if (digits.Length == 0)
- return;
-
- _isPasting = true;
- for (int i = 0; i < _otpBoxes.Length && i < digits.Length; i++)
- {
- _otpBoxes[i].Text = digits[i].ToString();
- }
- _isPasting = false;
-
- int focusIndex = System.Math.Min(digits.Length, _otpBoxes.Length - 1);
- _otpBoxes[focusIndex].Focus();
-
- TryAutoSubmit();
- }
-
- private void otp_TextChanged(object sender, TextChangedEventArgs e)
- {
- if (_isPasting)
- return;
-
- var box = sender as TextBox;
- int index = System.Array.IndexOf(_otpBoxes, box);
-
- // Filter non-digit
- string digits = new string(box.Text.Where(char.IsDigit).ToArray());
- if (digits != box.Text)
- {
- box.Text = digits;
- box.CaretIndex = digits.Length;
- return;
- }
-
- if (box.Text.Length == 1 && index < _otpBoxes.Length - 1)
- {
- _otpBoxes[index + 1].Focus();
- }
-
- TryAutoSubmit();
- }
-
- private void otp_GotFocus(object sender, RoutedEventArgs e)
- {
- TextBox box = sender as TextBox;
- box.SelectAll();
- }
-
- private void TryAutoSubmit()
- {
- if (btn_login.IsEnabled && _otpBoxes.All(b => b.Text.Length == 1))
- {
- btn_login.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
- }
- }
- }
-}
diff --git a/Beanfun/Pages/LoginWait.xaml b/Beanfun/Pages/LoginWait.xaml
deleted file mode 100644
index 7949140..0000000
--- a/Beanfun/Pages/LoginWait.xaml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/LoginWait.xaml.cs b/Beanfun/Pages/LoginWait.xaml.cs
deleted file mode 100644
index 4d8f343..0000000
--- a/Beanfun/Pages/LoginWait.xaml.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Beanfun
-{
- ///
- /// LoginWait.xaml 的交互逻辑
- ///
- public partial class LoginWait : Page
- {
- public LoginWait()
- {
- InitializeComponent();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.loginWorker.CancelAsync();
- App.MainWnd.totpWorker.CancelAsync();
- App.MainWnd.bfAPPAutoLogin.IsEnabled = false;
- t_Info.Content = TryFindResource("MsgLogging") as string;
- if (App.LoginMethod == (int)LoginMethod.QRCode)
- App.MainWnd.loginMethodChanged();
- App.MainWnd.return_page = App.MainWnd.loginPage;
- }
- }
-}
diff --git a/Beanfun/Pages/ManageAccount.xaml b/Beanfun/Pages/ManageAccount.xaml
deleted file mode 100644
index 09d9535..0000000
--- a/Beanfun/Pages/ManageAccount.xaml
+++ /dev/null
@@ -1,308 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/ManageAccount.xaml.cs b/Beanfun/Pages/ManageAccount.xaml.cs
deleted file mode 100644
index cba9926..0000000
--- a/Beanfun/Pages/ManageAccount.xaml.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-using System.Collections.Generic;
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Beanfun
-{
- ///
- /// ManagerAccount.xaml 的交互逻辑
- ///
- public partial class ManageAccount : Page
- {
- public class BeanfunAccount
- {
- public string account { get; set; }
- public string accountname { get; set; }
- public string isSavePwd { get; set; }
- public string isAutoLogin { get; set; }
- public string isSaveVerify { get; set; }
-
- public BeanfunAccount()
- {
- this.account = null;
- this.accountname = null;
- this.isSavePwd = null;
- this.isAutoLogin = null;
- this.isSaveVerify = null;
- }
-
- public BeanfunAccount(
- string account,
- string accountname,
- string isSavePwd,
- string isAutoLogin,
- string isSaveVerify = null
- )
- {
- this.account = account;
- this.accountname = accountname;
- this.isSavePwd = isSavePwd;
- this.isAutoLogin = isAutoLogin;
- this.isSaveVerify = isSaveVerify;
- }
- }
-
- public ManageAccount()
- {
- InitializeComponent();
- }
-
- public void setupAccList(MainWindow MainWnd)
- {
- string region = !btn_TW.IsEnabled ? "TW" : "HK";
- string[] accList = MainWnd.accountManager.getAccountList(region);
- List accountList = new List();
- foreach (string s in accList)
- {
- accountList.Add(
- new BeanfunAccount(
- s,
- MainWnd.accountManager.getNameByAccount(region, s),
- MainWnd.accountManager.getPasswordByAccount(region, s) != ""
- ? TryFindResource("Yes") as string
- : TryFindResource("No") as string,
- MainWnd.accountManager.getAutoLoginByAccount(region, s)
- ? TryFindResource("Yes") as string
- : TryFindResource("No") as string,
- MainWnd.accountManager.getVerifyByAccount(region, s) != ""
- ? TryFindResource("Yes") as string
- : TryFindResource("No") as string
- )
- );
- }
- list_Account.ItemsSource = null;
- list_Account.ItemsSource = accountList;
- if (accList.Length > 0)
- list_Account.SelectedIndex = 0;
- else
- {
- btn_Change.IsEnabled = false;
- btn_Delete.IsEnabled = false;
- }
- }
-
- private void TW_Button_Click(object sender, RoutedEventArgs e)
- {
- if (!btn_TW.IsEnabled)
- return;
- btn_TW.IsEnabled = false;
- btn_HK.IsEnabled = true;
- setupAccList(App.MainWnd);
- }
-
- private void HK_Button_Click(object sender, RoutedEventArgs e)
- {
- if (!btn_HK.IsEnabled)
- return;
- btn_TW.IsEnabled = true;
- btn_HK.IsEnabled = false;
- setupAccList(App.MainWnd);
- }
-
- private void Up_Button_Click(object sender, RoutedEventArgs e)
- {
- if (list_Account.SelectedIndex <= 0)
- return;
- changeAccountIndex(true);
- }
-
- private void Down_Button_Click(object sender, RoutedEventArgs e)
- {
- if (list_Account.SelectedIndex + 1 >= list_Account.Items.Count)
- return;
- changeAccountIndex(false);
- }
-
- private void changeAccountIndex(bool up)
- {
- string region = !btn_TW.IsEnabled ? "TW" : "HK";
- string account = ((BeanfunAccount)list_Account.SelectedItem).account;
- string name = App.MainWnd.accountManager.getNameByAccount(region, account);
- string password = App.MainWnd.accountManager.getPasswordByAccount(region, account);
- string verify = App.MainWnd.accountManager.getVerifyByAccount(region, account);
- int method = App.MainWnd.accountManager.getMethodByAccount(region, account);
- bool autoLogin = App.MainWnd.accountManager.getAutoLoginByAccount(region, account);
- int changedIndex = list_Account.SelectedIndex + (up ? -1 : 1);
- App.MainWnd.accountManager.addAccount(
- changedIndex,
- region,
- account,
- name,
- password,
- verify,
- method,
- autoLogin
- );
- setupAccList(App.MainWnd);
- App.MainWnd.loginMethodInit();
- list_Account.SelectedIndex = changedIndex;
- }
-
- private void Add_Button_Click(object sender, RoutedEventArgs e)
- {
- if (!btn_Add.IsEnabled)
- return;
- AddAccount wnd = new AddAccount();
- wnd.ShowDialog();
- }
-
- private void Delete_Button_Click(object sender, RoutedEventArgs e)
- {
- if (!btn_Delete.IsEnabled)
- return;
-
- string t_acc_del = "";
- if (list_Account.SelectedItems.Count < 1)
- return;
- else if (list_Account.SelectedItems.Count > 1)
- t_acc_del = string.Format(
- TryFindResource("MsgDeleteAccountMulti") as string,
- list_Account.SelectedItems.Count
- );
- else
- t_acc_del = string.Format(
- TryFindResource("MsgDeleteAccountSingle") as string,
- ((BeanfunAccount)list_Account.SelectedItem).account
- );
- MessageBoxResult result = MessageBox.Show(
- string.Format(TryFindResource("MsgDeleteAccountMng") as string, t_acc_del),
- TryFindResource("DeleteAccount") as string,
- MessageBoxButton.YesNo
- );
-
- if (result == MessageBoxResult.Yes)
- {
- string region = !btn_TW.IsEnabled ? "TW" : "HK";
- foreach (BeanfunAccount acc in list_Account.SelectedItems)
- {
- App.MainWnd.accountManager.removeAccount(region, acc.account);
- }
- setupAccList(App.MainWnd);
- App.MainWnd.loginMethodInit();
- }
- }
-
- private void Return_Button_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.frame.Content = App.MainWnd.settingPage;
- }
-
- private void list_Account_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- btn_Up.IsEnabled = false;
- btn_Down.IsEnabled = false;
- if (list_Account.Items.Count <= 0)
- {
- btn_Change.IsEnabled = false;
- btn_Delete.IsEnabled = false;
- return;
- }
-
- if (list_Account.SelectedIndex > 0)
- btn_Up.IsEnabled = true;
-
- if (list_Account.SelectedIndex + 1 < list_Account.Items.Count)
- btn_Down.IsEnabled = true;
-
- btn_Change.IsEnabled = true;
- btn_Delete.IsEnabled = true;
- }
-
- private void Change_Button_Click(object sender, RoutedEventArgs e)
- {
- if (!btn_Change.IsEnabled)
- return;
- ChangeAccount wnd = new ChangeAccount(
- list_Account.SelectedIndex,
- !btn_TW.IsEnabled ? "TW" : "HK",
- ((BeanfunAccount)list_Account.SelectedItem).account
- );
- wnd.ShowDialog();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- new AccRecovery(App.MainWnd.accountManager).ShowDialog();
- }
- }
-}
diff --git a/Beanfun/Pages/Settings.xaml b/Beanfun/Pages/Settings.xaml
deleted file mode 100644
index f1c2408..0000000
--- a/Beanfun/Pages/Settings.xaml
+++ /dev/null
@@ -1,229 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/Settings.xaml.cs b/Beanfun/Pages/Settings.xaml.cs
deleted file mode 100644
index 44966de..0000000
--- a/Beanfun/Pages/Settings.xaml.cs
+++ /dev/null
@@ -1,294 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Beanfun
-{
- ///
- /// Settings.xaml 的交互逻辑
- ///
- public partial class Settings : Page
- {
- class LanguageItem
- {
- public string Name { get; set; }
- public string DisplayName { get; set; }
- }
-
- public Settings()
- {
- InitializeComponent();
-
- List languageList = new List();
- languageList.Add(new LanguageItem { Name = "zh-Hant", DisplayName = "中文(繁體)" });
- languageList.Add(new LanguageItem { Name = "zh-Hans", DisplayName = "中文(简体)" });
- languageList.Add(new LanguageItem { Name = "en", DisplayName = "English" });
- cb_Language.ItemsSource = languageList;
- cb_Language.DisplayMemberPath = "DisplayName";
- cb_Language.SelectedValuePath = "Name";
- string cultureName = I18n.CultureName.ToUpper();
- string name = null;
- foreach (LanguageItem language in languageList)
- {
- if (language.Name.ToUpper().Equals(cultureName))
- {
- name = language.Name;
- break;
- }
- }
- if (name == null)
- {
- name = "zh-Hant";
- switch (cultureName)
- {
- case "ZH-CHS":
- case "ZH-CN":
- case "ZH-SG":
- case "ZH-MY":
- case "ZH-HANS-HK":
- case "ZH-HANS-MO":
- name = "zh-Hans";
- break;
- }
- }
- cb_Language.SelectedValue = name;
-
- autoStartGame.IsChecked = bool.Parse(
- ConfigAppSettings.GetValue("autoStartGame", "false")
- );
- ask_update.IsChecked = bool.Parse(ConfigAppSettings.GetValue("ask_update", "true"));
- minimize_to_tray.IsChecked = bool.Parse(
- ConfigAppSettings.GetValue("minimize_to_tray", "false")
- );
- disableHardwareAcceleration.IsChecked = bool.Parse(
- ConfigAppSettings.GetValue("disableHardwareAcceleration", "false")
- );
-
- tradLogin.IsChecked = bool.Parse(ConfigAppSettings.GetValue("tradLogin", "true"));
- skipPlayWnd.IsChecked = bool.Parse(ConfigAppSettings.GetValue("skipPlayWnd", "true"));
- autoKillPatcher.IsChecked = bool.Parse(
- ConfigAppSettings.GetValue("autoKillPatcher", "true")
- );
-
- cb_UpdateChannel.SelectedIndex = ConfigAppSettings
- .GetValue("updateChannel", "Stable")
- .Equals("Stable")
- ? 0
- : 1;
-
- cb_ThemeColor.Text = ConfigAppSettings.GetValue("ThemeColor", "#FF8201");
-
- cb_LoginMode.SelectedIndex = App.LoginMethod == (int)LoginMethod.Regular ? 0 : 1;
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (App.MainWnd == null || App.MainWnd.frame == null)
- return;
- if (App.MainWnd.return_page == null || App.MainWnd.return_page == App.MainWnd.loginPage)
- App.MainWnd.NavigateLoginPage();
- else
- App.MainWnd.frame.Content = App.MainWnd.return_page;
- App.MainWnd.return_page = null;
- }
-
- private void skipPlayWnd_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || App.MainWnd.checkPlayPage == null
- || skipPlayWnd.IsChecked
- == bool.Parse(ConfigAppSettings.GetValue("skipPlayWnd", "true"))
- )
- return;
- ConfigAppSettings.SetValue(
- "skipPlayWnd",
- Convert.ToString((bool)skipPlayWnd.IsChecked)
- );
- App.MainWnd.checkPlayPage.IsEnabled = (bool)skipPlayWnd.IsChecked;
- }
-
- private void autoKillPatcher_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || autoKillPatcher.IsChecked
- == bool.Parse(ConfigAppSettings.GetValue("autoKillPatcher", "true"))
- )
- return;
- ConfigAppSettings.SetValue(
- "autoKillPatcher",
- Convert.ToString((bool)autoKillPatcher.IsChecked)
- );
- App.MainWnd.checkPatcher.IsEnabled = (bool)autoKillPatcher.IsChecked;
- }
-
- private void autoStartGame_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || autoStartGame.IsChecked
- == bool.Parse(ConfigAppSettings.GetValue("autoStartGame", "false"))
- )
- return;
- ConfigAppSettings.SetValue(
- "autoStartGame",
- Convert.ToString((bool)autoStartGame.IsChecked)
- );
- }
-
- private void ask_update_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || ask_update.IsChecked
- == bool.Parse(ConfigAppSettings.GetValue("ask_update", "true"))
- )
- return;
- ConfigAppSettings.SetValue("ask_update", Convert.ToString((bool)ask_update.IsChecked));
- }
-
- private void tradLogin_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.accountList == null
- || App.MainWnd.accountList.panel_GetOtp == null
- || App.MainWnd.accountList.autoPaste == null
- )
- return;
- if ((bool)tradLogin.IsChecked)
- {
- App.MainWnd.accountList.panel_GetOtp.Visibility = Visibility.Visible;
-
- if ("MapleStoryClass".Equals(App.MainWnd.win_class_name))
- App.MainWnd.accountList.autoPaste.Visibility = Visibility.Visible;
- else
- App.MainWnd.accountList.autoPaste.Visibility = Visibility.Collapsed;
- }
- else
- {
- App.MainWnd.accountList.panel_GetOtp.Visibility = Visibility.Collapsed;
- }
- if (
- App.MainWnd.settingPage == null
- || bool.Parse(ConfigAppSettings.GetValue("tradLogin", "true"))
- == tradLogin.IsChecked
- )
- return;
- ConfigAppSettings.SetValue("tradLogin", Convert.ToString(tradLogin.IsChecked));
- }
-
- private void minimize_to_tray_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || minimize_to_tray.IsChecked
- == bool.Parse(ConfigAppSettings.GetValue("minimize_to_tray", "false"))
- )
- return;
- ConfigAppSettings.SetValue(
- "minimize_to_tray",
- Convert.ToString((bool)minimize_to_tray.IsChecked)
- );
- }
-
- private void disableHardwareAcceleration_CheckedChanged(object sender, RoutedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || disableHardwareAcceleration.IsChecked
- == bool.Parse(
- ConfigAppSettings.GetValue("disableHardwareAcceleration", "false")
- )
- )
- return;
- ConfigAppSettings.SetValue(
- "disableHardwareAcceleration",
- Convert.ToString((bool)disableHardwareAcceleration.IsChecked)
- );
- MessageBox.Show(
- TryFindResource("MsgRestartForHardwareAccel") as string,
- TryFindResource("MsgRestartForHardwareAccelTitle") as string,
- MessageBoxButton.OK,
- MessageBoxImage.Information
- );
- }
-
- private void cb_UpdateChannel_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || cb_UpdateChannel.SelectedIndex
- == (
- ConfigAppSettings.GetValue("updateChannel", "Stable").Equals("Stable")
- ? 0
- : 1
- )
- )
- return;
- ConfigAppSettings.SetValue(
- "updateChannel",
- cb_UpdateChannel.SelectedIndex == 0 ? "Stable" : "Beta"
- );
- }
-
- private void cb_ThemeColor_TextChanged(object sender, System.EventArgs e)
- {
- try
- {
- App.MainWnd.changeThemeColor(cb_ThemeColor.Text);
- ConfigAppSettings.SetValue("ThemeColor", cb_ThemeColor.Text);
- }
- catch { }
- }
-
- private void cb_LoginMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || cb_LoginMode.SelectedIndex
- == (ConfigAppSettings.GetValue("loginMethod", "0").Equals("0") ? 0 : 1)
- )
- return;
- ConfigAppSettings.SetValue("loginMethod", cb_LoginMode.SelectedIndex == 0 ? "0" : "1");
- }
-
- private void ManageAcc_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.frame.Content = App.MainWnd.manageAccPage;
- }
-
- private void btn_Tools_Click(object sender, RoutedEventArgs e)
- {
- if (App.MainWnd.accountList != null)
- App.MainWnd.accountList.btn_Tools_Click(null, null);
- }
-
- private void cb_Language_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- string language = ConfigAppSettings.GetValue("Language", null);
- if (
- App.MainWnd == null
- || App.MainWnd.settingPage == null
- || (
- language != null
- && cb_Language.SelectedValue.ToString().ToUpper().Equals(language.ToUpper())
- )
- )
- return;
- language = cb_Language.SelectedValue.ToString();
- ConfigAppSettings.SetValue("Language", language);
- I18n.LoadLanguage(language);
- }
- }
-}
diff --git a/Beanfun/Pages/VerifyPage.xaml b/Beanfun/Pages/VerifyPage.xaml
deleted file mode 100644
index 704a070..0000000
--- a/Beanfun/Pages/VerifyPage.xaml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/VerifyPage.xaml.cs b/Beanfun/Pages/VerifyPage.xaml.cs
deleted file mode 100644
index 1ad701c..0000000
--- a/Beanfun/Pages/VerifyPage.xaml.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// VerifyPage.xaml 的交互逻辑
- ///
- public partial class VerifyPage : Page
- {
- public VerifyPage()
- {
- InitializeComponent();
- }
-
- private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (App.LoginMethod == (int)LoginMethod.QRCode)
- App.MainWnd.loginMethodChanged();
- App.MainWnd.return_page = App.MainWnd.loginPage;
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (this.t_Verify.Text.Length <= 0)
- {
- MessageBox.Show(TryFindResource("MsgAuthInfoEmpty") as string);
- return;
- }
- if (this.t_Code.Text.Length <= 0)
- {
- MessageBox.Show(TryFindResource("MsgCaptchaCodeEmpty") as string);
- return;
- }
-
- App.MainWnd.verifyWorker.RunWorkerAsync();
- }
-
- private void Button_Click_1(object sender, RoutedEventArgs e)
- {
- imageCaptcha.Source = App.MainWnd.bfClient.getVerifyCaptcha(App.MainWnd.samplecaptcha);
- t_Code.Text = "";
- }
- }
-}
diff --git a/Beanfun/Pages/gamepass_form.xaml b/Beanfun/Pages/gamepass_form.xaml
deleted file mode 100644
index b35b82a..0000000
--- a/Beanfun/Pages/gamepass_form.xaml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/gamepass_form.xaml.cs b/Beanfun/Pages/gamepass_form.xaml.cs
deleted file mode 100644
index e17147e..0000000
--- a/Beanfun/Pages/gamepass_form.xaml.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Beanfun
-{
- ///
- /// gamepass_form.xaml 的交互逻辑
- ///
- public partial class gamepass_form : Page
- {
- public gamepass_form()
- {
- InitializeComponent();
- }
-
- private async void btn_OpenGamePass_Click(object sender, RoutedEventArgs e)
- {
- btn_OpenGamePass.IsEnabled = false;
- try
- {
- var client = new BeanfunClient();
- string skey = await System.Threading.Tasks.Task.Run(() => client.GetSessionkey());
- if (string.IsNullOrEmpty(skey))
- {
- MessageBox.Show(
- Application.Current.TryFindResource("SessionKeyFailed") as string
- );
- return;
- }
- App.MainWnd.bfClient = client;
- new GamePassBrowser(skey).Show();
- }
- catch
- {
- MessageBox.Show(Application.Current.TryFindResource("ConnectionFailed") as string);
- }
- finally
- {
- btn_OpenGamePass.IsEnabled = true;
- }
- }
-
- private void btn_back_Click(object sender, RoutedEventArgs e)
- {
- App.LoginMethod = (int)LoginMethod.Regular;
- App.MainWnd.loginMethodChanged();
- }
- }
-}
diff --git a/Beanfun/Pages/id-pass_form.xaml b/Beanfun/Pages/id-pass_form.xaml
deleted file mode 100644
index 1de4126..0000000
--- a/Beanfun/Pages/id-pass_form.xaml
+++ /dev/null
@@ -1,810 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/id-pass_form.xaml.cs b/Beanfun/Pages/id-pass_form.xaml.cs
deleted file mode 100644
index 7ff8b4d..0000000
--- a/Beanfun/Pages/id-pass_form.xaml.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-using System.Windows;
-using System.Windows.Controls;
-
-namespace Beanfun
-{
- ///
- /// id_pass_form.xaml 的交互逻辑
- ///
- public partial class id_pass_form : Page
- {
- public id_pass_form()
- {
- InitializeComponent();
-
- this.Loaded += (sender, e) =>
- {
- var tb =
- t_AccountID.Template.FindName("PART_EditableTextBox", t_AccountID) as TextBox;
- if (tb != null)
- System.Windows.Input.InputMethod.SetPreferredImeState(
- tb,
- System.Windows.Input.InputMethodState.Off
- );
- };
- }
-
- private void checkBox_RememberPWD_Unchecked(object sender, RoutedEventArgs e)
- {
- checkBox_AutoLogin.IsChecked = false;
- }
-
- private void checkBox_AutoLogin_Checked(object sender, RoutedEventArgs e)
- {
- checkBox_RememberPWD.IsChecked = true;
- }
-
- private void RegAcc_Click(object sender, RoutedEventArgs e)
- {
- string url;
- if (App.LoginRegion == "TW")
- {
- url = "https://tw.beanfun.com/TW/signup/Join_beanfun_signup.aspx?service=999999_T0";
- }
- else
- {
- url =
- "https://bfweb.hk.beanfun.com/beanfun_web_ap/signup/preregistration.aspx?service=999999_T0";
- }
- new WebBrowser(url).Show();
- }
-
- private void FindPwd_Click(object sender, RoutedEventArgs e)
- {
- string url;
- if (App.LoginRegion == "TW")
- {
- url = "https://tw.beanfun.com/member/forgot_pwd.aspx";
- }
- else
- {
- url = "https://bfweb.hk.beanfun.com/member/forgot_pwd.aspx";
- }
- new WebBrowser(url).Show();
- }
-
- private void btn_login_Click(object sender, RoutedEventArgs e)
- {
- if (t_AccountID.Text == null || t_AccountID.Text == "")
- {
- MessageBox.Show(TryFindResource("AccountNeed") as string);
- return;
- }
- if (t_Password.Password == null || t_Password.Password == "")
- {
- MessageBox.Show(TryFindResource("PasswordNeed") as string);
- return;
- }
- //System.Console.WriteLine("PW" + t_Password.Password);
- App.MainWnd.do_Login();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- new GameList().ShowDialog();
- }
-
- private void t_AccountID_TextChanged(object sender, System.EventArgs e)
- {
- TextBox tb =
- t_AccountID.Template.FindName("PART_EditableTextBox", t_AccountID) as TextBox;
- int caretIndex = 0;
- if (tb != null)
- caretIndex = tb.CaretIndex;
-
- string tbAccount = t_AccountID.Text;
-
- Regex regex = new Regex(@"\((.*)\)");
- if (regex.IsMatch(tbAccount))
- {
- t_AccountID.Text = regex.Match(tbAccount).Groups[1].Value;
- tb.Text = t_AccountID.Text;
- }
-
- List searches = new List();
- List accList = new List();
- string[] accArr = App.MainWnd.accountManager.getAccountList(App.LoginRegion);
-
- bool IsFind = false;
- foreach (string s in accArr)
- {
- if (s == t_AccountID.Text)
- {
- IsFind = true;
- }
- accList.Add(s);
- }
-
- searches = accList.FindAll(
- delegate(string s)
- {
- return s.Contains(t_AccountID.Text.Trim());
- }
- );
-
- for (int i = 0; i < accList.Count; i++)
- {
- string name = App.MainWnd.accountManager.getNameByAccount(
- App.LoginRegion,
- accList[i]
- );
- if (name != null && name != "")
- {
- if (tbAccount == accList[i])
- tbAccount = name + "(" + accList[i] + ")";
- accList[i] = name + "(" + accList[i] + ")";
- }
- }
-
- for (int i = 0; i < searches.Count; i++)
- {
- string name = App.MainWnd.accountManager.getNameByAccount(
- App.LoginRegion,
- searches[i]
- );
- if (name != null && name != "")
- {
- searches[i] = name + "(" + searches[i] + ")";
- }
- }
-
- if (!IsFind && t_AccountID.Text != "" && searches.Count > 0)
- {
- t_AccountID.IsDropDownOpen = true;
- t_AccountID.ItemsSource = null;
- t_AccountID.ItemsSource = searches;
- t_AccountID.SelectedIndex = -1;
- t_AccountID.Text = tbAccount;
- }
- else
- {
- t_AccountID.ItemsSource = null;
- t_AccountID.ItemsSource = accList;
- if (!IsFind)
- {
- t_AccountID.SelectedIndex = -1;
- t_AccountID.Text = tbAccount;
- }
- t_AccountID.IsDropDownOpen = false;
-
- if (IsFind)
- {
- if (accList.Count > 0)
- t_AccountID.SelectedItem = tbAccount;
-
- t_Password.Password = "";
- checkBox_RememberPWD.IsChecked = false;
-
- int loginMethod = App.MainWnd.accountManager.getMethodByAccount(
- App.LoginRegion,
- t_AccountID.Text
- );
- if (loginMethod > -1)
- App.LoginMethod = loginMethod;
- App.MainWnd.loginMethodChanged();
- }
- }
-
- if (tb != null)
- tb.CaretIndex = caretIndex;
- }
-
- private void t_AccountID_GotFocus(object sender, RoutedEventArgs e)
- {
- var tb = t_AccountID.Template.FindName("PART_EditableTextBox", t_AccountID) as TextBox;
- if (tb != null)
- {
- tb.CaretIndex = tb.Text.Length;
- }
- }
-
- private void t_AccountID_DropDown(object sender, System.EventArgs e)
- {
- var tb = t_AccountID.Template.FindName("PART_EditableTextBox", t_AccountID) as TextBox;
- if (tb != null)
- {
- string tbAccount = t_AccountID.Text;
- string name = App.MainWnd.accountManager.getNameByAccount(
- App.LoginRegion,
- tbAccount
- );
- if (name != null && name != "")
- tbAccount = name + "(" + tbAccount + ")";
- if (
- (t_AccountID.ItemsSource as List)
- .FindAll(
- delegate(string s)
- {
- return s.Equals(tbAccount.Trim());
- }
- )
- .Count <= 0
- )
- tb.SelectionLength = 0;
- else
- tb.CaretIndex = tb.Text.Length;
- }
- }
-
- private void DeleteButton_Click(object sender, RoutedEventArgs e)
- {
- string t_AccID = t_AccountID.Text;
- Button closeButton = sender as Button;
- string t_AccID_toDelete = (string)closeButton.Tag;
-
- Regex regex = new Regex(@"\((.*)\)");
- if (regex.IsMatch(t_AccID_toDelete))
- t_AccID_toDelete = regex.Match(t_AccID_toDelete).Groups[1].Value;
-
- MessageBoxResult result = MessageBox.Show(
- string.Format(TryFindResource("MsgDeleteAccount") as string, t_AccID_toDelete),
- TryFindResource("DeleteAccount") as string,
- MessageBoxButton.YesNo
- );
-
- if (result == MessageBoxResult.Yes)
- {
- App.MainWnd.accountManager.removeAccount(App.LoginRegion, t_AccID_toDelete);
- App.MainWnd.loginMethodInit();
-
- foreach (string s in t_AccountID.Items)
- {
- string str = s;
- if (regex.IsMatch(str))
- str = regex.Match(str).Groups[1].Value;
- if (t_AccID == str)
- {
- t_AccountID.SelectedItem = str;
- break;
- }
- }
- }
- }
-
- private void btn_QRCode_Click(object sender, RoutedEventArgs e)
- {
- App.LoginMethod = (int)LoginMethod.QRCode;
- App.MainWnd.loginMethodChanged();
- }
-
- private async void btn_GamePass_Click(object sender, RoutedEventArgs e)
- {
- btn_GamePass.IsEnabled = false;
- try
- {
- var client = new BeanfunClient();
- string skey = await System.Threading.Tasks.Task.Run(() => client.GetSessionkey());
- if (string.IsNullOrEmpty(skey))
- {
- MessageBox.Show(TryFindResource("SessionKeyFailed") as string);
- return;
- }
- App.MainWnd.bfClient = client;
- new GamePassBrowser(skey).Show();
- }
- catch
- {
- MessageBox.Show(TryFindResource("ConnectionFailed") as string);
- }
- finally
- {
- btn_GamePass.IsEnabled = true;
- }
- }
-
- private void btn_StartGame_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.runGame();
- }
- }
-}
diff --git a/Beanfun/Pages/qr_form.xaml b/Beanfun/Pages/qr_form.xaml
deleted file mode 100644
index badddce..0000000
--- a/Beanfun/Pages/qr_form.xaml
+++ /dev/null
@@ -1,249 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Pages/qr_form.xaml.cs b/Beanfun/Pages/qr_form.xaml.cs
deleted file mode 100644
index e61c076..0000000
--- a/Beanfun/Pages/qr_form.xaml.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media.Imaging;
-
-namespace Beanfun
-{
- ///
- /// qr_form.xaml 的交互逻辑
- ///
- public partial class qr_form : Page
- {
- public qr_form()
- {
- InitializeComponent();
- }
-
- private void btn_Refresh_QRCode_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.refreshQRCode();
- }
-
- private void btn_Refresh_QRCode_MouseEnter(object sender, MouseEventArgs e)
- {
- if (qr_Tip.Visibility == Visibility.Collapsed)
- {
- DockPanel.SetDock(btn_Refresh_QRCode, Dock.Left);
- qr_Tip.Visibility = Visibility.Visible;
- }
- }
-
- private void qr_Tip_Click(object sender, RoutedEventArgs e)
- {
- Process.Start(
- new ProcessStartInfo(
- "https://tw.beanfun.com/bfevent/bfApp/Page20160930/PC/index.html"
- )
- {
- UseShellExecute = true,
- }
- );
- }
-
- private void TextBlock_MouseLeave(object sender, MouseEventArgs e)
- {
- if (qr_Tip.Visibility == Visibility.Visible)
- {
- DockPanel.SetDock(btn_Refresh_QRCode, Dock.Top);
- qr_Tip.Visibility = Visibility.Collapsed;
- }
- }
-
- private void btn_CopyDeeplink_Click(object sender, RoutedEventArgs e)
- {
- var qrcodeClass = App.MainWnd.qrcodeClass;
- if (qrcodeClass != null && !string.IsNullOrEmpty(qrcodeClass.deeplink))
- {
- try
- {
- WindowsAPI.CopyText(qrcodeClass.deeplink);
- MessageBox.Show(
- Application.Current.TryFindResource("CopyDeeplinkSuccess") as string
- );
- }
- catch
- {
- MessageBox.Show(Application.Current.TryFindResource("CopyFailed") as string);
- }
- }
- else
- {
- MessageBox.Show(
- Application.Current.TryFindResource("CopyDeeplinkNotReady") as string
- );
- }
- }
-
- private void btn_back_Click(object sender, RoutedEventArgs e)
- {
- App.LoginMethod = (int)LoginMethod.Regular;
- App.MainWnd.loginMethodChanged();
- }
-
- private void btn_StartGame_Click(object sender, RoutedEventArgs e)
- {
- App.MainWnd.runGame();
- }
-
- private void CopyQRCode_Click(object sender, RoutedEventArgs e)
- {
- if (qr_image.Source is BitmapSource bmp)
- {
- bool ok = false;
- try
- {
- var encoder = new PngBitmapEncoder();
- encoder.Frames.Add(BitmapFrame.Create(bmp));
- using (var stream = new System.IO.MemoryStream())
- {
- encoder.Save(stream);
- stream.Position = 0;
- var bitmap = new System.Drawing.Bitmap(stream);
- System.Windows.Forms.Clipboard.SetImage(bitmap);
- ok = true;
- }
- }
- catch { }
- ShowToast(
- Application.Current.TryFindResource(ok ? "CopyQRCodeSuccess" : "CopyFailed")
- as string
- ?? (ok ? "QR Code copied!" : "Copy failed"),
- ok
- );
- }
- }
-
- private Window _enlargeWnd;
-
- public void CloseEnlargeWindow()
- {
- if (_enlargeWnd != null)
- {
- _enlargeWnd.Close();
- _enlargeWnd = null;
- }
- }
-
- private void EnlargeQRCode_Click(object sender, RoutedEventArgs e)
- {
- if (qr_image.Source == null)
- return;
-
- CloseEnlargeWindow();
-
- _enlargeWnd = new Window
- {
- Title = "QR Code",
- Width = 350,
- Height = 350,
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
- Owner = Window.GetWindow(this),
- ResizeMode = ResizeMode.CanResize,
- Content = new Image
- {
- Source = qr_image.Source,
- Stretch = System.Windows.Media.Stretch.Uniform,
- },
- };
- _enlargeWnd.Closed += (s, _) => _enlargeWnd = null;
- _enlargeWnd.Show();
- }
-
- private void ShowToast(string message, bool success = true)
- {
- toastText.Text = (success ? "✓ " : "") + message;
- toastBorder.Background = new System.Windows.Media.SolidColorBrush(
- (System.Windows.Media.Color)
- System.Windows.Media.ColorConverter.ConvertFromString(
- success ? "#CC2E7D32" : "#CC333333"
- )
- );
- toastBorder.Visibility = Visibility.Visible;
- var timer = new System.Windows.Threading.DispatcherTimer
- {
- Interval = TimeSpan.FromSeconds(2),
- };
- timer.Tick += (s, _) =>
- {
- timer.Stop();
- toastBorder.Visibility = Visibility.Collapsed;
- };
- timer.Start();
- }
- }
-}
diff --git a/Beanfun/Properties/AssemblyInfo.cs b/Beanfun/Properties/AssemblyInfo.cs
deleted file mode 100644
index f156674..0000000
--- a/Beanfun/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// 有关程序集的一般信息由以下
-// 控制。更改这些特性值可修改
-// 与程序集关联的信息。
-[assembly: AssemblyTitle("繽放")]
-[assembly: AssemblyDescription("第三方樂豆客戶端")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Pungin")]
-[assembly: AssemblyProduct("Beanfun")]
-[assembly: AssemblyCopyright("Copyright © Pungin 2017-2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-//将 ComVisible 设置为 false 将使此程序集中的类型
-//对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型,
-//请将此类型的 ComVisible 特性设置为 true。
-[assembly: ComVisible(false)]
-
-// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
-[assembly: Guid("24238732-c686-4921-9f1c-5159a4344638")]
-
-// 程序集的版本信息由下列四个值组成:
-//
-// 主版本
-// 次版本
-// 生成号
-// 修订号
-//
-[assembly: AssemblyVersion("5.9.*")]
-//[assembly: AssemblyFileVersion("0.0.0.0")]
-[assembly: NeutralResourcesLanguage("zh-Hant")]
-
-[assembly: System.Reflection.AssemblyInformationalVersion("5.9.1(2604180731)")]
diff --git a/Beanfun/Properties/Resources.Designer.cs b/Beanfun/Properties/Resources.Designer.cs
deleted file mode 100644
index 109b5a2..0000000
--- a/Beanfun/Properties/Resources.Designer.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// 此代码由工具生成。
-// 运行时版本:4.0.30319.42000
-//
-// 对此文件的更改可能会导致不正确的行为,并且如果
-// 重新生成代码,这些更改将会丢失。
-//
-//------------------------------------------------------------------------------
-
-namespace Beanfun.Properties {
- using System;
-
-
- ///
- /// 一个强类型的资源类,用于查找本地化的字符串等。
- ///
- // 此类是由 StronglyTypedResourceBuilder
- // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
- // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
- // (以 /str 作为命令选项),或重新生成 VS 项目。
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resources {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resources() {
- }
-
- ///
- /// 返回此类使用的缓存的 ResourceManager 实例。
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Beanfun.Properties.Resources", typeof(Resources).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// 重写当前线程的 CurrentUICulture 属性,对
- /// 使用此强类型资源类的所有资源查找执行重写。
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- ///
- /// 查找类似于 (图标) 的 System.Drawing.Icon 类型的本地化资源。
- ///
- internal static System.Drawing.Icon icon {
- get {
- object obj = ResourceManager.GetObject("icon", resourceCulture);
- return ((System.Drawing.Icon)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap QRCode_Tip {
- get {
- object obj = ResourceManager.GetObject("QRCode_Tip", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap refresh {
- get {
- object obj = ResourceManager.GetObject("refresh", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_Black {
- get {
- object obj = ResourceManager.GetObject("Scroll_Black", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_BM {
- get {
- object obj = ResourceManager.GetObject("Scroll_BM", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_Glory {
- get {
- object obj = ResourceManager.GetObject("Scroll_Glory", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_JD {
- get {
- object obj = ResourceManager.GetObject("Scroll_JD", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_Other {
- get {
- object obj = ResourceManager.GetObject("Scroll_Other", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_Red {
- get {
- object obj = ResourceManager.GetObject("Scroll_Red", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_SM {
- get {
- object obj = ResourceManager.GetObject("Scroll_SM", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_V {
- get {
- object obj = ResourceManager.GetObject("Scroll_V", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Drawing.Bitmap 类型的本地化资源。
- ///
- internal static System.Drawing.Bitmap Scroll_X {
- get {
- object obj = ResourceManager.GetObject("Scroll_X", resourceCulture);
- return ((System.Drawing.Bitmap)(obj));
- }
- }
-
- ///
- /// 查找 System.Byte[] 类型的本地化资源。
- ///
- internal static byte[] segmdl2 {
- get {
- object obj = ResourceManager.GetObject("segmdl2", resourceCulture);
- return ((byte[])(obj));
- }
- }
- }
-}
diff --git a/Beanfun/Properties/Resources.resx b/Beanfun/Properties/Resources.resx
deleted file mode 100644
index 08d2368..0000000
--- a/Beanfun/Properties/Resources.resx
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
-
- ..\Resources\icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\refresh.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_Black.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_JD.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_Other.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_Red.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_V.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_X.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_BM.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_SM.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\Scroll_Glory.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
- ..\Resources\segmdl2.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- ..\Resources\QRCode_Tip.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
-
\ No newline at end of file
diff --git a/Beanfun/Properties/Settings.Designer.cs b/Beanfun/Properties/Settings.Designer.cs
deleted file mode 100644
index 40f7d1f..0000000
--- a/Beanfun/Properties/Settings.Designer.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// 此代码由工具生成。
-// 运行时版本:4.0.30319.42000
-//
-// 对此文件的更改可能会导致不正确的行为,并且如果
-// 重新生成代码,这些更改将会丢失。
-//
-//------------------------------------------------------------------------------
-
-namespace Beanfun.Properties {
-
-
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")]
- internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
-
- private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
-
- public static Settings Default {
- get {
- return defaultInstance;
- }
- }
- }
-}
diff --git a/Beanfun/Properties/Settings.settings b/Beanfun/Properties/Settings.settings
deleted file mode 100644
index 8e615f2..0000000
--- a/Beanfun/Properties/Settings.settings
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/Beanfun/Properties/app.manifest b/Beanfun/Properties/app.manifest
deleted file mode 100644
index 72bbf1e..0000000
--- a/Beanfun/Properties/app.manifest
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Beanfun/Resources/QRCode_Tip.png b/Beanfun/Resources/QRCode_Tip.png
deleted file mode 100644
index 7e10360..0000000
Binary files a/Beanfun/Resources/QRCode_Tip.png and /dev/null differ
diff --git a/Beanfun/Resources/icon.ico b/Beanfun/Resources/icon.ico
deleted file mode 100644
index aefe543..0000000
Binary files a/Beanfun/Resources/icon.ico and /dev/null differ
diff --git a/Beanfun/Resources/refresh.png b/Beanfun/Resources/refresh.png
deleted file mode 100644
index a324883..0000000
Binary files a/Beanfun/Resources/refresh.png and /dev/null differ
diff --git a/Beanfun/Resources/segmdl2.ttf b/Beanfun/Resources/segmdl2.ttf
deleted file mode 100644
index beae244..0000000
Binary files a/Beanfun/Resources/segmdl2.ttf and /dev/null differ
diff --git a/Beanfun/Tools/BeanfunClient.Account.cs b/Beanfun/Tools/BeanfunClient.Account.cs
deleted file mode 100644
index c2e4101..0000000
--- a/Beanfun/Tools/BeanfunClient.Account.cs
+++ /dev/null
@@ -1,688 +0,0 @@
-using System.Collections.Specialized;
-using System.Net;
-using System.Text.RegularExpressions;
-using Newtonsoft.Json.Linq;
-
-namespace Beanfun
-{
- partial class BeanfunClient : WebClient
- {
- public class ServiceAccount
- {
- public bool isEnable { get; set; }
- public bool visible { get; set; }
- public bool isinherited { get; set; }
- public string sid { get; set; }
- public string ssn { get; set; }
- public string sname { get; set; }
- public string screatetime { get; set; }
- public string slastusedtime { get; set; }
- public string sauthtype { get; set; }
-
- public ServiceAccount(
- bool isEnable,
- string sid,
- string ssn,
- string sname,
- string screatetime
- )
- {
- this.isEnable = isEnable;
- this.visible = true;
- this.isinherited = false;
- this.sid = sid;
- this.ssn = ssn;
- this.sname = sname;
- this.screatetime = screatetime;
- this.slastusedtime = null;
- this.sauthtype = null;
- }
-
- public ServiceAccount(
- bool isEnable,
- bool visible,
- bool isinherited,
- string sid,
- string ssn,
- string sname,
- string screatetime,
- string slastusedtime,
- string sauthtype
- )
- {
- this.isEnable = isEnable;
- this.visible = visible;
- this.isinherited = isinherited;
- this.sid = sid;
- this.ssn = ssn;
- this.sname = sname;
- this.screatetime = screatetime;
- this.slastusedtime = slastusedtime;
- this.sauthtype = sauthtype;
- }
- }
-
- public void GetAccounts(string service_code, string service_region, bool fatal = true)
- {
- if (this.WebToken == null)
- return;
-
- string host;
- if (App.LoginRegion == "TW")
- host = "tw.beanfun.com";
- else
- host = "bfweb.hk.beanfun.com";
-
- Regex regex;
-
- this.DownloadString(
- $"https://{host}/beanfun_block/auth.aspx?channel=game_zone&page_and_query=game_start.aspx%3Fservice_code_and_region%3D{service_code}_{service_region}&web_token={WebToken}"
- );
-
- string response = this.DownloadString(
- $"https://{host}/beanfun_block/game_zone/game_server_account_list.aspx?sc={service_code}&sr={service_region}&dt={GetCurrentTime(2)}"
- );
-
- // Add account list to ListView.
- regex = new Regex(
- "onclick=\"([^\"]*)\">(.*)
"
- );
- if (regex.IsMatch(response))
- {
- accountAmountLimitNotice = regex.Match(response).Groups[1].Value;
- if (accountAmountLimitNotice.Contains("進階認證"))
- accountAmountLimitNotice =
- System.Windows.Application.Current.TryFindResource("AuthReLogin") as string;
- else
- accountAmountLimitNotice = I18n.ToSimplified(accountAmountLimitNotice);
- }
- else
- accountAmountLimitNotice = "";
-
- if (this.accountList.Count > 0)
- {
- // sort by ssn as default order
- this.accountList.Sort(
- (x, y) =>
- {
- return x.ssn.CompareTo(y.ssn);
- }
- );
-
- // then apply the user-defined order
- string gameCode = service_code + "_" + service_region;
- AccountList.ApplyAccountOrder(this.accountList, gameCode);
- }
-
- this.errmsg = null;
- }
-
- private string GetCreateTime(string service_code, string service_region, string sn)
- {
- try
- {
- string response = this.DownloadString(
- "https://"
- + (App.LoginRegion == "TW" ? "tw.beanfun.com" : "bfweb.hk.beanfun.com")
- + "/beanfun_block/game_zone/game_start_step2.aspx?service_code="
- + service_code
- + "&service_region="
- + service_region
- + "&sotp="
- + sn
- + "&dt="
- + GetCurrentTime(2)
- );
- Regex regex = new Regex("ServiceAccountCreateTime: \"([^\"]+)\"");
- if (!regex.IsMatch(response))
- {
- return null;
- }
- return regex.Match(response).Groups[1].Value;
- }
- catch
- {
- return null;
- }
- }
-
- private NameValueCollection UnconnectedGame_InitAccountPayload(
- string service_code,
- string service_region
- )
- {
- string strUrl = "https://";
- string response;
- if (App.LoginRegion == "TW")
- strUrl += "tw.beanfun.com/TW/";
- else
- strUrl += "bfweb.hk.beanfun.com/HK/";
- response = this.DownloadString(
- $"{strUrl}auth.aspx?channel=accounts_management&page_and_query=01.aspx%3FServiceCode%3D{service_code}%26ServiceRegion%3D{service_region}&web_token={WebToken}"
- );
-
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- string viewstategenerator = regex.Match(response).Groups[1].Value;
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
-
- return payload;
- }
-
- public NameValueCollection UnconnectedGame_InitAddAccountPayload(
- string service_code,
- string service_region
- )
- {
- NameValueCollection payload = UnconnectedGame_InitAccountPayload(
- service_code,
- service_region
- );
- payload.Add("__EVENTTARGET", "");
- payload.Add("__EVENTARGUMENT", "");
- payload.Add("imgbtn_AddAccount.x", "0");
- payload.Add("imgbtn_AddAccount.y", "0");
-
- string response;
- if (App.LoginRegion == "TW")
- response = this.UploadString(
- "https://tw.beanfun.com/TW/accounts_management/02.aspx",
- payload
- );
- else
- response = this.UploadString(
- "https://bfweb.hk.beanfun.com/HK/accounts_management/02.aspx",
- payload
- );
-
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- string viewstategenerator = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
-
- payload.Clear();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
-
- regex = new Regex("(.*)");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoGameName";
- return null;
- }
- payload.Add("GameName", regex.Match(response).Groups[1].Value);
-
- regex = new Regex("(.*)");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoAccountLen";
- return null;
- }
- payload.Add("AccountLen", regex.Match(response).Groups[1].Value);
- payload.Add(
- "CheckNickName",
- response.Contains("");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- string viewstategenerator = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
-
- payload.Clear();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
-
- regex = new Regex("(.*)");
- payload.Add(
- "lblErrorMessage",
- regex.IsMatch(response) ? regex.Match(response).Groups[1].Value : ""
- );
-
- return payload;
- }
-
- public NameValueCollection UnconnectedGame_AddAccountCheckNickName(
- string service_code,
- string service_region,
- string txtServiceAccountDN,
- NameValueCollection payload
- )
- {
- if (payload == null)
- return null;
- payload.Add("__EVENTTARGET", "lbtnCheckNickName");
- payload.Add("__EVENTARGUMENT", "");
- payload.Add("txtServiceAccountID", "");
- if (txtServiceAccountDN != null)
- {
- if (App.LoginRegion == "TW")
- payload.Add("t1", txtServiceAccountDN);
- else
- payload.Add("txtServiceAccountDN", txtServiceAccountDN);
- }
- payload.Add("txtNewPwd", "");
- payload.Add("txtNewPwd2", "");
- string response;
- if (App.LoginRegion == "TW")
- response = this.UploadString(
- "https://tw.beanfun.com/TW/accounts_management/02.aspx",
- payload
- );
- else
- response = this.UploadString(
- "https://bfweb.hk.beanfun.com/HK/accounts_management/02.aspx",
- payload
- );
-
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- string viewstategenerator = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
-
- payload.Clear();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
-
- regex = new Regex("(.*)");
- payload.Add(
- "lblErrorMessage",
- regex.IsMatch(response) ? regex.Match(response).Groups[1].Value : ""
- );
-
- return payload;
- }
-
- public string UnconnectedGame_AddAccount(
- string service_code,
- string service_region,
- string name,
- string txtNewPwd,
- string txtNewPwd2,
- string txtServiceAccountDN,
- NameValueCollection payload
- )
- {
- if (name == null || name == "")
- return null;
- if (txtNewPwd == null || txtNewPwd == "")
- return null;
- if (txtNewPwd2 == null || txtNewPwd2 == "")
- return null;
- if (payload == null)
- return null;
-
- payload.Add("__EVENTTARGET", "");
- payload.Add("__EVENTARGUMENT", "");
- payload.Add("txtServiceAccountID", name);
- if (txtServiceAccountDN != null)
- {
- if (App.LoginRegion == "TW")
- payload.Add("t1", txtServiceAccountDN);
- else
- payload.Add("txtServiceAccountDN", txtServiceAccountDN);
- }
- payload.Add("txtNewPwd", txtNewPwd);
- payload.Add("txtNewPwd2", txtNewPwd2);
- payload.Add("chkBox1", "on");
- payload.Add("imgbtn_Submit.x", "0");
- payload.Add("imgbtn_Submit.y", "0");
-
- string response;
- if (App.LoginRegion == "TW")
- response = this.UploadString(
- "https://tw.beanfun.com/TW/accounts_management/02.aspx",
- payload
- );
- else
- response = this.UploadString(
- "https://bfweb.hk.beanfun.com/HK/accounts_management/02.aspx",
- payload
- );
- Regex regex = new Regex(
- "(.*)"
- );
-
- return regex.IsMatch(response) ? regex.Match(response).Groups[1].Value : "";
- }
-
- public string UnconnectedGame_ChangePassword(
- string service_code,
- string service_region,
- int num,
- string txtEmail
- )
- {
- UnconnectedGame_InitAccountPayload(service_code, service_region);
-
- string response;
- if (App.LoginRegion == "TW")
- response = this.DownloadString(
- "https://tw.beanfun.com/TW/accounts_management/01Accounts.aspx"
- );
- else
- response = this.DownloadString(
- "https://bfweb.hk.beanfun.com/HK/accounts_management/01Accounts.aspx"
- );
-
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- string viewstategenerator = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
- payload.Add("__EVENTTARGET", "gvServiceAccountList");
- payload.Add("__EVENTARGUMENT", "ChangePassword$" + num);
- payload.Add("x", "0");
- payload.Add("y", "0");
-
- if (App.LoginRegion == "TW")
- {
- response = this.UploadString(
- "https://tw.beanfun.com/TW/accounts_management/01Accounts.aspx",
- payload
- );
- response = this.DownloadString(
- "https://tw.beanfun.com/TW/accounts_management/03.aspx"
- );
- }
- else
- {
- response = this.UploadString(
- "http://bfweb.hk.beanfun.com/HK/accounts_management/01Accounts.aspx",
- payload
- );
- response = this.DownloadString(
- "http://bfweb.hk.beanfun.com/HK/accounts_management/03.aspx"
- );
- }
-
- regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- viewstate = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstategenerator";
- return null;
- }
- viewstategenerator = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- eventvalidation = regex.Match(response).Groups[1].Value;
-
- payload.Clear();
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstategenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
- payload.Add("txtEmail", txtEmail);
- payload.Add("imgbtn_Submit.x", "0"); //12
- payload.Add("imgbtn_Submit.y", "0");
-
- regex = new Regex("(.*)");
- if (App.LoginRegion == "TW")
- response = this.UploadString(
- "https://tw.beanfun.com/TW/accounts_management/03.aspx",
- payload
- );
- else
- response = this.UploadString(
- "http://bfweb.hk.beanfun.com/HK/accounts_management/03.aspx",
- payload
- );
-
- string lblErrorMessage = regex.IsMatch(response)
- ? regex.Match(response).Groups[1].Value
- : "";
- if (lblErrorMessage != "")
- return lblErrorMessage;
-
- regex = new Regex("verify_code=(.*)");
- return regex.IsMatch(this.ResponseUri.ToString())
- ? ("verify_code" + regex.Match(this.ResponseUri.ToString()).Groups[1].Value)
- : null;
- }
-
- public bool AddServiceAccount(string name, string service_code, string service_region)
- {
- if (name == null || name == "")
- return false;
- NameValueCollection payload = new NameValueCollection();
- payload.Add("strFunction", "AddServiceAccount");
- payload.Add("npsc", "");
- payload.Add("npsr", "");
- payload.Add("sc", service_code);
- payload.Add("sr", service_region);
- payload.Add("sadn", name);
- payload.Add("sag", "");
-
- string response = this.UploadString(
- $"https://{(App.LoginRegion == "TW" ? "tw" : "bfweb.hk")}.beanfun.com/generic_handlers/gamezone.ashx",
- payload
- );
- if (response == "")
- return false;
- JObject jsonData = JObject.Parse(response);
- if (jsonData["intResult"] == null || (int)jsonData["intResult"] != 1)
- return false;
- else
- return true;
- }
-
- public bool ChangeServiceAccountDisplayName(
- string newName,
- string gameCode,
- ServiceAccount account
- )
- {
- if (newName == null || newName == "" || account == null || newName == account.sname)
- {
- return false;
- }
- NameValueCollection payload = new NameValueCollection();
- payload.Add("strFunction", "ChangeServiceAccountDisplayName");
- payload.Add("sl", gameCode);
- payload.Add("said", account.sid);
- payload.Add("nsadn", newName);
-
- string response = this.UploadString(
- $"https://{(App.LoginRegion == "TW" ? "tw" : "bfweb.hk")}.beanfun.com/generic_handlers/gamezone.ashx",
- payload
- );
- if (response == "")
- return false;
- JObject jsonData = JObject.Parse(response);
- if (jsonData["intResult"] == null || (int)jsonData["intResult"] != 1)
- return false;
- else
- return true;
- }
-
- public string GetServiceContract(string service_code, string service_region)
- {
- NameValueCollection payload = new NameValueCollection();
- payload.Add("strFunction", "GetServiceContract");
- payload.Add("sc", service_code);
- payload.Add("sr", service_region);
- string response = this.UploadStringGZip(
- $"https://{(App.LoginRegion == "TW" ? "tw" : "bfweb.hk")}.beanfun.com/generic_handlers/gamezone.ashx",
- payload
- );
- if (response == "")
- return "";
- JObject jsonData = JObject.Parse(response);
- if (jsonData["intResult"] == null || (int)jsonData["intResult"] != 1)
- return "";
- else
- return (string)jsonData["strResult"];
- }
- }
-}
diff --git a/Beanfun/Tools/BeanfunClient.Login.cs b/Beanfun/Tools/BeanfunClient.Login.cs
deleted file mode 100644
index e5ab6a3..0000000
--- a/Beanfun/Tools/BeanfunClient.Login.cs
+++ /dev/null
@@ -1,928 +0,0 @@
-using System;
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Text.RegularExpressions;
-using System.Web;
-using System.Windows.Media.Imaging;
-using Newtonsoft.Json.Linq;
-
-namespace Beanfun
-{
- public partial class BeanfunClient : WebClient
- {
- private string RegularLogin(
- string id,
- string pass,
- string skey,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- if (App.LoginRegion == "TW")
- return TwRegularLogin(id, pass, skey, service_code, service_region);
- else
- return HkRegularLogin(id, pass, skey);
- }
-
- private string TwRegularLogin(
- string id,
- string pass,
- string skey,
- string service_code,
- string service_region
- )
- {
- try
- {
- string apiBase = "https://login.beanfun.com";
- string indexUrl = $"{apiBase}/Login/Index?pSKey={skey}";
-
- // Step 1: Get index page and antiforgery token
- SetBaseHeaders(false, "text/html");
- string indexHtml = this.DownloadString(indexUrl);
- string formToken = Regex
- .Match(indexHtml, "__RequestVerificationToken[^>]+value=\"([^\"]+)\"")
- .Groups[1]
- .Value;
-
- if (string.IsNullOrEmpty(formToken))
- {
- this.errmsg = "LoginNoToken";
- return null;
- }
-
- // Step 2: Check account type
- SetJsonHeaders(formToken, indexUrl);
- string checkTypeBody = new JObject
- {
- ["Account"] = id,
- ["Captcha"] = "",
- ["__RequestVerificationToken"] = formToken,
- }.ToString(Newtonsoft.Json.Formatting.None);
- string checkTypeRes = this.UploadString(
- $"{apiBase}/Login/CheckAccountType?pSKey={skey}",
- "POST",
- checkTypeBody
- );
-
- string captchaToken = "";
- if (
- !string.IsNullOrWhiteSpace(checkTypeRes)
- && checkTypeRes.TrimStart().StartsWith("{")
- )
- {
- var checkJson = JObject.Parse(checkTypeRes);
- captchaToken = checkJson["ResultData"]?["Captcha"]?.ToString() ?? "";
- }
-
- // Step 3: Account login
- SetJsonHeaders(formToken, indexUrl);
- string loginBody = new JObject
- {
- ["Account"] = id,
- ["Pasw"] = pass,
- ["IsMobile"] = false,
- ["Captcha"] = captchaToken,
- ["__RequestVerificationToken"] = formToken,
- }.ToString(Newtonsoft.Json.Formatting.None);
- string loginRes = this.UploadString(
- $"{apiBase}/Login/AccountLogin?pSKey={skey}",
- "POST",
- loginBody
- );
-
- var loginJson = JObject.Parse(loginRes);
- string resultCode = loginJson["ResultCode"]?.ToString();
- string result = loginJson["Result"]?.ToString();
- string resultMsg = loginJson["ResultMessage"]?.ToString() ?? "";
-
- if (resultCode == "1")
- {
- if (result == "1")
- {
- this.errmsg = "LoginAdvanceCheck";
- return null;
- }
-
- // Use SendLogin flow (same as QRCode) to get bfWebToken
- SetBaseHeaders(
- true,
- "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
- indexUrl
- );
- string sendLoginHtml = this.DownloadString($"{apiBase}/Login/SendLogin");
-
- NameValueCollection payload = new NameValueCollection();
- foreach (
- Match tag in Regex.Matches(
- sendLoginHtml,
- @"]+>",
- RegexOptions.IgnoreCase | RegexOptions.Singleline
- )
- )
- {
- string tagStr = tag.Value;
- Match nameMatch = Regex.Match(
- tagStr,
- @"name\s*=\s*['""]([^'""]+)['""]",
- RegexOptions.IgnoreCase
- );
- Match valMatch = Regex.Match(
- tagStr,
- @"value\s*=\s*['""]([^'""]*)['""]",
- RegexOptions.IgnoreCase
- );
- if (
- nameMatch.Success
- && valMatch.Success
- && tagStr.IndexOf("type=\"submit\"", StringComparison.OrdinalIgnoreCase)
- == -1
- )
- payload.Add(nameMatch.Groups[1].Value, valMatch.Groups[1].Value);
- }
-
- if (payload.Count == 0)
- {
- this.errmsg = "SendLoginNoFormData";
- return null;
- }
-
- this.redirect = false;
- SetBaseHeaders(true, null, $"{apiBase}/");
- string returnResponse = this.UploadString(
- "https://tw.beanfun.com/beanfun_block/bflogin/return.aspx",
- payload
- );
- string setCookieHeader = this.ResponseHeaders?["Set-Cookie"];
- if (!string.IsNullOrEmpty(setCookieHeader))
- {
- Match tokenMatch = Regex.Match(setCookieHeader, @"bfWebToken=([^;]+)");
- if (tokenMatch.Success)
- this.webtoken = tokenMatch.Groups[1].Value;
- }
- this.redirect = true;
-
- if (string.IsNullOrEmpty(this.webtoken))
- {
- this.errmsg = "LoginNoWebtoken";
- return null;
- }
-
- GetAccounts(service_code, service_region, false);
- if (this.errmsg != null)
- return null;
-
- this.remainPoint = getRemainPoint();
- this.errmsg = null;
- return null; // return null with no errmsg = success, skip LoginCompleted
- }
-
- // ResultCode 2: AdvanceCheck required — reuse existing VerifyPage flow
- if (resultCode == "2")
- {
- if (resultMsg.StartsWith("http"))
- this.advanceCheckUrl = resultMsg;
- this.errmsg = "LoginAdvanceCheck";
- return null;
- }
-
- this.errmsg = resultMsg;
- return null;
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"[TwRegularLogin] Exception: {ex.Message}");
- this.errmsg = "LoginUnknown\n\n" + ex.Message + "\n" + ex.StackTrace;
- return null;
- }
- }
-
- private string HkRegularLogin(string id, string pass, string skey)
- {
- string loginHost = "login.hk.beanfun.com";
- try
- {
- string url = $"https://{loginHost}/login/id-pass_form_newBF.aspx?otp1={skey}";
- string response = this.DownloadString(url);
-
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return null;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
-
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return null;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
-
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstateGenerator";
- return null;
- }
- string viewstateGenerator = regex.Match(response).Groups[1].Value;
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("__EVENTTARGET", "");
- payload.Add("__EVENTARGUMENT", "");
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstateGenerator);
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
- payload.Add("t_AccountID", id);
- payload.Add("t_Password", pass);
- payload.Add("btn_login", "登入");
-
- response = this.UploadString(url, payload);
-
- if (response.Contains("RELOAD_CAPTCHA_CODE") && response.Contains("alert"))
- {
- this.errmsg = "LoginAdvanceCheck";
- return null;
- }
-
- if (response.Contains("totpLoginBtn"))
- {
- this.totpResponse = response;
- this.totpUrl = url;
- this.errmsg = "need_totp";
- return null;
- }
-
- regex = new Regex("akey=(.*)");
- if (!regex.IsMatch(this.ResponseUri.ToString()))
- {
- this.errmsg = "LoginNoAkey";
- regex = new Regex(
- ""
- );
- if (regex.IsMatch(response))
- {
- this.errmsg = regex.Match(response).Groups[1].Value;
- }
- else
- {
- regex = new Regex("pollRequest\\(\"([^\"]*)\",\"(\\w+)\",\"([^\"]+)\"\\);");
- if (regex.IsMatch(response))
- {
- this.errmsg =
- regex.Match(response).Groups[1].Value
- + "\",\""
- + regex.Match(response).Groups[3].Value;
- LoginToken = regex.Match(response).Groups[2].Value;
- }
- }
- return null;
- }
- return regex.Match(this.ResponseUri.ToString()).Groups[1].Value;
- }
- catch (Exception e)
- {
- this.errmsg = "LoginUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- return null;
- }
- }
-
- private void SetJsonHeaders(string verificationToken, string referer)
- {
- SetBaseHeaders(true, "application/json, text/plain, */*", referer);
- this.Headers.Add("X-Requested-With", "XMLHttpRequest");
- this.Headers.Add("RequestVerificationToken", verificationToken);
- this.Headers[HttpRequestHeader.ContentType] = "application/json; charset=utf-8";
- }
-
- public void TotpLogin(
- string otp1,
- string otp2,
- string otp3,
- string otp4,
- string otp5,
- string otp6,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- string loginHost = this.totpUrl;
-
- try
- {
- string response = this.totpResponse;
- Regex regex = new Regex("id=\"__VIEWSTATE\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstate";
- return;
- }
- string viewstate = regex.Match(response).Groups[1].Value;
-
- regex = new Regex("id=\"__EVENTVALIDATION\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoEventvalidation";
- return;
- }
- string eventvalidation = regex.Match(response).Groups[1].Value;
- regex = new Regex("id=\"__VIEWSTATEGENERATOR\" value=\"(.*)\" />");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoViewstateGenerator";
- return;
- }
- string viewstateGenerator = regex.Match(response).Groups[1].Value;
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("__EVENTTARGET", "");
- payload.Add("__EVENTARGUMENT", "");
- payload.Add("__VIEWSTATE", viewstate);
- payload.Add("__VIEWSTATEGENERATOR", viewstateGenerator);
- if (App.LoginRegion == "HK")
- payload.Add("__VIEWSTATEENCRYPTED", "");
- payload.Add("__EVENTVALIDATION", eventvalidation);
- payload.Add("otpCode1", otp1);
- payload.Add("otpCode2", otp2);
- payload.Add("otpCode3", otp3);
- payload.Add("otpCode4", otp4);
- payload.Add("otpCode5", otp5);
- payload.Add("otpCode6", otp6);
- payload.Add("totpLoginBtn", "登入");
-
- response = this.UploadString(loginHost, payload);
- if (response.Contains("RELOAD_CAPTCHA_CODE") && response.Contains("alert"))
- {
- this.errmsg = "LoginAdvanceCheck";
- return;
- }
-
- regex = new Regex("akey=(.*)");
- if (!regex.IsMatch(this.ResponseUri.ToString()))
- {
- this.errmsg = "LoginNoAkey";
- regex = new Regex(
- ""
- );
- if (regex.IsMatch(response))
- {
- this.errmsg = regex.Match(response).Groups[1].Value;
- }
- else
- {
- regex = new Regex("pollRequest\\(\"([^\"]*)\",\"(\\w+)\",\"([^\"]+)\"\\);");
- if (regex.IsMatch(response))
- {
- this.errmsg =
- regex.Match(response).Groups[1].Value
- + "\",\""
- + regex.Match(response).Groups[3].Value;
- LoginToken = regex.Match(response).Groups[2].Value;
- }
- }
- return;
- }
- string akey = regex.Match(this.ResponseUri.ToString()).Groups[1].Value;
-
- LoginCompleted(akey, service_code, service_region);
- }
- catch (Exception e)
- {
- this.errmsg = "LoginUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- return;
- }
- }
-
- public class QRCodeClass
- {
- public string skey;
- public string bitmapBase64;
- public string deeplink;
- public string requestVerificationToken;
- }
-
- public QRCodeClass GetQRCodeValue(string skey)
- {
- SetBaseHeaders(false, "text/html");
- string url = $"https://login.beanfun.com/Login/Index?pSKey={skey}";
- string response = this.DownloadString(url);
- // Extract RequestVerificationToken from login page for QR polling
- string verificationToken = null;
- Match tokenMatch = Regex.Match(
- response,
- @"__RequestVerificationToken[^>]+value=""([^""]+)"""
- );
- if (tokenMatch.Success)
- verificationToken = tokenMatch.Groups[1].Value;
- JObject strEncryptData = this.getQRCodeStrEncryptData(skey);
- if (strEncryptData == null)
- {
- this.errmsg = "LoginIntResultError";
- return null;
- }
-
- JObject result = (JObject)strEncryptData["ResultData"];
- if (result == null)
- {
- this.errmsg = "LoginIntResultError";
- return null;
- }
-
- string base64Image = (string)result["QRImage"];
- if (string.IsNullOrEmpty(base64Image))
- {
- this.errmsg = "LoginIntResultError";
- return null;
- }
-
- string deepLinkRaw = result["DeepLink"]?.Value();
- string deeplink = NormalizeBeanfunAppDeeplink(deepLinkRaw);
-
- return new QRCodeClass
- {
- skey = skey,
- bitmapBase64 = "data:image/png;base64," + base64Image,
- deeplink = deeplink,
- requestVerificationToken = verificationToken,
- };
- }
-
- public JObject getQRCodeStrEncryptData(string skey)
- {
- SetBaseHeaders(
- true,
- "application/json, text/plain, */*",
- $"https://login.beanfun.com/Login/Index?pSKey={skey}"
- );
- this.Headers.Add("X-Requested-With", "XMLHttpRequest");
- this.Headers.Add("Origin", "https://login.beanfun.com");
- string response = this.DownloadString(
- $"https://login.beanfun.com/Login/InitLogin?pSKey={skey}"
- );
- JObject jsonData = JObject.Parse(response);
-
- if (jsonData["Result"] == null || (int)jsonData["Result"] != 0)
- {
- this.errmsg = "LoginIntResultError";
- return null;
- }
-
- return jsonData;
- }
-
- private string NormalizeBeanfunAppDeeplink(string raw)
- {
- if (string.IsNullOrWhiteSpace(raw))
- return raw;
-
- if (!Uri.TryCreate(raw.Trim(), UriKind.Absolute, out Uri uri))
- return raw;
-
- if (
- !string.Equals(
- uri.Host,
- "play.games.gamania.com",
- StringComparison.OrdinalIgnoreCase
- )
- )
- return raw;
-
- if (uri.AbsolutePath.IndexOf("deeplink", StringComparison.OrdinalIgnoreCase) < 0)
- return raw;
-
- NameValueCollection query = HttpUtility.ParseQueryString(uri.Query);
- string inner = query["url"];
- if (!string.IsNullOrEmpty(inner))
- return inner;
-
- return raw;
- }
-
- public BitmapImage getQRCodeImage(QRCodeClass qrcodeclass)
- {
- try
- {
- byte[] bytes = Convert.FromBase64String(
- qrcodeclass.bitmapBase64.Replace("data:image/png;base64,", "")
- );
-
- BitmapImage image = new BitmapImage();
- using (var ms = new MemoryStream(bytes))
- {
- image.BeginInit();
- image.CacheOption = BitmapCacheOption.OnLoad;
- image.StreamSource = ms;
- image.EndInit();
- }
- return image;
- }
- catch
- {
- return null;
- }
- }
-
- private string QRCodeLogin(QRCodeClass qrcodeclass)
- {
- try
- {
- string skey = qrcodeclass.skey;
- SetBaseHeaders(
- true,
- "application/json, text/plain, */*",
- $"https://login.beanfun.com/Login/Index?pSKey={skey}"
- );
- string response = this.DownloadString("https://login.beanfun.com/QRLogin/QRLogin");
- Debug.WriteLine("QRLogin response: " + response);
-
- SetBaseHeaders(
- true,
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
- $"https://login.beanfun.com/Login/Index?pSKey={skey}"
- );
- string sendLoginHtml = this.DownloadString(
- "https://login.beanfun.com/Login/SendLogin"
- );
-
- NameValueCollection payload = new NameValueCollection();
- foreach (
- Match tag in Regex.Matches(
- sendLoginHtml,
- @"]+>",
- RegexOptions.IgnoreCase | RegexOptions.Singleline
- )
- )
- {
- string tagStr = tag.Value;
- Match nameMatch = Regex.Match(
- tagStr,
- @"name\s*=\s*['""]([^'""]+)['""]",
- RegexOptions.IgnoreCase
- );
-
- Match valMatch = Regex.Match(
- tagStr,
- @"value\s*=\s*['""]([^'""]*)['""]",
- RegexOptions.IgnoreCase
- );
- if (
- nameMatch.Success
- && valMatch.Success
- && tagStr.IndexOf("type=\"submit\"", StringComparison.OrdinalIgnoreCase)
- == -1
- )
- payload.Add(nameMatch.Groups[1].Value, valMatch.Groups[1].Value);
- }
-
- if (payload.Count == 0)
- {
- errmsg = "SendLoginNoFormData";
- return null;
- }
-
- this.redirect = false;
- SetBaseHeaders(true, null, "https://login.beanfun.com/");
- string returnUrl = "https://tw.beanfun.com/beanfun_block/bflogin/return.aspx";
- string returnResponse = this.UploadString(returnUrl, payload);
- string setCookieHeader = this.ResponseHeaders?["Set-Cookie"];
- if (!string.IsNullOrEmpty(setCookieHeader))
- {
- Match tokenMatch = Regex.Match(setCookieHeader, @"bfWebToken=([^;]+)");
- if (tokenMatch.Success)
- this.webtoken = tokenMatch.Groups[1].Value;
- }
- this.redirect = true;
- return "OK";
- }
- catch (Exception e)
- {
- this.errmsg = "LoginUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- return null;
- }
- }
-
- public int QRCodeCheckLoginStatus(QRCodeClass qrcodeclass)
- {
- try
- {
- string skey = qrcodeclass.skey;
-
- SetBaseHeaders(
- true,
- "application/json, text/plain, */*",
- $"https://login.beanfun.com/Login/Index?pSKey={skey}"
- );
- this.Headers.Set("Origin", "https://login.beanfun.com");
- this.Headers.Set("RequestVerificationToken", qrcodeclass.requestVerificationToken);
-
- NameValueCollection payload = new NameValueCollection();
- string response = this.UploadString(
- "https://login.beanfun.com/QRLogin/CheckLoginStatus",
- payload
- );
-
- JObject jsonData;
- try
- {
- jsonData = JObject.Parse(response);
- }
- catch
- {
- this.errmsg = "LoginJsonParseFailed";
- return -1;
- }
-
- string result = (string)jsonData["ResultMessage"];
- Console.WriteLine(result);
-
- if (result == "Failed" || result == "Wait Login")
- return 0;
- else if (result == "Token Expired")
- return -2;
- else if (result == "Success")
- return 1;
- else
- {
- this.errmsg = response;
- return -1;
- }
- }
- catch (Exception e)
- {
- this.errmsg =
- "Network Error on QRCode checking login status\n\n"
- + e.Message
- + "\n"
- + e.StackTrace;
- }
-
- return -1;
- }
-
- public JObject CheckIsRegisteDevice(
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- NameValueCollection payload = new NameValueCollection();
- payload.Add("LT", LoginToken);
-
- string response = this.UploadString(
- "https://tw.newlogin.beanfun.com/login/bfAPPAutoLogin.ashx",
- payload
- );
- JObject json = JObject.Parse(response);
- if (json == null || json["IntResult"] == null || json["StrReslut"] == null)
- return null;
-
- if ((string)json["IntResult"] == "2")
- {
- string test = this.DownloadString(
- "https://tw.newlogin.beanfun.com/login/" + (string)json["StrReslut"]
- );
- Regex regex = new Regex("akey=(.*)");
- if (!regex.IsMatch((string)json["StrReslut"]))
- {
- this.errmsg = "AKeyParseFailed";
- return null;
- }
- string akey = regex.Match((string)json["StrReslut"]).Groups[1].Value;
-
- LoginCompleted(akey, service_code, service_region);
- }
-
- return json;
- }
-
- public string GetSessionkey()
- {
- if (App.LoginRegion == "TW")
- {
- this.DownloadString(
- "https://tw.beanfun.com/beanfun_block/bflogin/default.aspx?service=999999_T0"
- );
- string finalUrl = this.ResponseUri?.ToString();
- if (string.IsNullOrEmpty(finalUrl))
- {
- this.errmsg = "LoginNoResponse";
- return null;
- }
-
- Regex regex = new Regex(@"[sp][Ss]?[Kk]ey=([^&]+)");
- if (!regex.IsMatch(finalUrl))
- {
- this.errmsg = "LoginNoSkey";
- return null;
- }
- return regex.Match(finalUrl).Groups[1].Value;
- }
- else
- {
- string response = this.DownloadString(
- "https://bfweb.hk.beanfun.com/beanfun_block/bflogin/default.aspx?service=999999_T0"
- );
- if (response == null)
- {
- this.errmsg = "LoginNoResponse";
- return null;
- }
- Regex regex = new Regex(
- "(.*)"
- );
- if (!regex.IsMatch(response))
- {
- this.errmsg = "LoginNoOTP1";
- return null;
- }
- return regex.Match(response).Groups[1].Value;
- }
- }
-
- public void Login(
- string id,
- string pass,
- int loginMethod,
- QRCodeClass qrcodeClass = null,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- this.webtoken = null;
- this.SessionKey = null;
- try
- {
- string akey = null;
- if (loginMethod == (int)LoginMethod.QRCode)
- {
- SessionKey = qrcodeClass.skey;
- }
- else
- {
- SessionKey = GetSessionkey();
- }
-
- switch (loginMethod)
- {
- case (int)LoginMethod.Regular:
- akey = RegularLogin(id, pass, SessionKey, service_code, service_region);
- break;
- case (int)LoginMethod.QRCode:
- akey = QRCodeLogin(qrcodeClass);
- break;
- default:
- this.errmsg = "LoginNoMethod";
- return;
- }
-
- LoginCompleted(akey, service_code, service_region);
- }
- catch (Exception e)
- {
- if (e is WebException)
- {
- this.errmsg =
- (
- System.Windows.Application.Current.TryFindResource(
- "NetworkConnectionError"
- ) as string
- ) + e.Message;
- }
- else
- {
- this.errmsg = "LoginUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- }
- return;
- }
- }
-
- public void GamePassLogin(
- string webToken,
- System.Collections.Generic.IEnumerable cookies,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- try
- {
- // Sync all cookies from WebView2 to BeanfunClient
- foreach (var cookie in cookies)
- {
- SetCookie(
- cookie.Name,
- cookie.Value,
- cookie.Domain.TrimStart('.'),
- string.IsNullOrEmpty(cookie.Path) ? "/" : cookie.Path
- );
- }
-
- this.webtoken = webToken;
-
- GetAccounts(service_code, service_region, false);
- if (this.errmsg != null)
- return;
-
- this.remainPoint = getRemainPoint();
- this.errmsg = null;
- }
- catch (Exception e)
- {
- this.errmsg = "LoginUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- }
- }
-
- private void LoginCompleted(
- string akey,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- if (this.SessionKey == null || akey == null)
- return;
-
- string host;
- if (App.LoginRegion == "TW")
- host = "tw.beanfun.com";
- else
- host = "bfweb.hk.beanfun.com";
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("SessionKey", this.SessionKey);
- payload.Add("AuthKey", akey);
- payload.Add("ServiceCode", "");
- payload.Add("ServiceRegion", "");
- payload.Add("ServiceAccountSN", "0");
- Debug.WriteLine(this.SessionKey);
- Debug.WriteLine(akey);
- string response = this.UploadString(
- $"https://{host}/beanfun_block/bflogin/return.aspx",
- payload
- );
- response = this.DownloadString($"https://{host}/{this.ResponseHeaders["Location"]}");
- Debug.WriteLine(this.ResponseHeaders);
-
- this.webtoken = this.GetCookie("bfWebToken");
- if (this.webtoken == "")
- {
- this.errmsg = "LoginNoWebtoken";
- return;
- }
- GetAccounts(service_code, service_region, false);
-
- if (this.errmsg != null)
- return;
-
- this.remainPoint = getRemainPoint();
-
- this.errmsg = null;
- }
-
- public void Logout()
- {
- string host;
- string loginHost;
- if (App.LoginRegion == "TW")
- {
- host = "tw.beanfun.com";
- loginHost = "tw.newlogin.beanfun.com";
- }
- else
- {
- host = "bfweb.hk.beanfun.com";
- loginHost = "login.hk.beanfun.com";
- }
- this.DownloadString($"https://{host}/generic_handlers/remove_bflogin_session.ashx");
- this.DownloadString($"https://{loginHost}/logout.aspx?service=999999_T0");
- if (App.LoginRegion == "TW")
- {
- NameValueCollection payload = new NameValueCollection();
- payload.Add("web_token", "1");
- this.UploadString(
- "https://tw.newlogin.beanfun.com/generic_handlers/erase_token.ashx",
- payload
- );
- }
- }
-
- private void SetBaseHeaders(
- bool withReferer = false,
- string accept = null,
- string referer = null
- )
- {
- this.Headers.Clear();
- this.Headers.Add(
- "User-Agent",
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
- );
- if (accept != null)
- this.Headers.Add("Accept", accept);
- if (withReferer && referer != null)
- this.Headers.Add("Referer", referer);
- }
- }
-}
diff --git a/Beanfun/Tools/BeanfunClient.OTP.cs b/Beanfun/Tools/BeanfunClient.OTP.cs
deleted file mode 100644
index b62cb2d..0000000
--- a/Beanfun/Tools/BeanfunClient.OTP.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using System;
-using System.Collections.Specialized;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using Newtonsoft.Json.Linq;
-
-namespace Beanfun
-{
- public partial class BeanfunClient : WebClient
- {
- public string GetOTP(
- ServiceAccount acc,
- string service_code = "610074",
- string service_region = "T9"
- )
- {
- try
- {
- string response;
- string host;
- string loginHost;
- if (App.LoginRegion == "TW")
- {
- host = "tw.beanfun.com";
- loginHost = "tw.newlogin.beanfun.com";
- }
- else
- {
- host = "bfweb.hk.beanfun.com";
- loginHost = "login.hk.beanfun.com";
- }
- response = this.DownloadString(
- $"https://{host}/beanfun_block/game_zone/game_start_step2.aspx?service_code={service_code}&service_region={service_region}&sotp={acc.ssn}&dt={GetCurrentTime(2)}"
- );
- Regex regex = new Regex("GetResultByLongPolling&key=(.*)\"");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "OTPNoLongPollingKey:" + response;
- return null;
- }
- string longPollingKey = regex.Match(response).Groups[1].Value;
- string unkKey = null;
- string unkValue = null;
- if (App.LoginRegion == "TW")
- {
- regex = new Regex("MyAccountData.ServiceAccountCreateTime \\+ \"(.*)=(.*)\";");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "OTPNoUnkData";
- return null;
- }
- unkKey = Uri.UnescapeDataString(regex.Match(response).Groups[1].Value);
- unkValue = Uri.UnescapeDataString(regex.Match(response).Groups[2].Value);
- }
- if (acc.screatetime == null)
- {
- regex = new Regex("ServiceAccountCreateTime: \"([^\"]+)\"");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "OTPNoCreateTime";
- return null;
- }
- acc.screatetime = regex.Match(response).Groups[1].Value;
- }
- response = this.DownloadString(
- $"https://{loginHost}/generic_handlers/get_cookies.ashx"
- );
-
- regex = new Regex("var m_strSecretCode = '(.*)';");
- if (!regex.IsMatch(response))
- {
- this.errmsg = "OTPNoSecretCode";
- return null;
- }
- string secretCode = regex.Match(response).Groups[1].Value;
-
- NameValueCollection payload = new NameValueCollection();
- payload.Add("service_code", service_code);
- payload.Add("service_region", service_region);
- payload.Add("service_account_id", acc.sid);
- payload.Add("sotp", acc.ssn);
- payload.Add("service_account_display_name", acc.sname);
- payload.Add("service_account_create_time", acc.screatetime);
- if (unkKey != null && unkValue != null)
- {
- payload.Add(unkKey, unkValue);
- }
- // testing...
- System.Net.ServicePointManager.Expect100Continue = false;
- this.UploadString(
- $"https://{host}/beanfun_block/generic_handlers/record_service_start.ashx",
- payload
- );
- response = this.DownloadString(
- $"https://{host}/generic_handlers/get_result.ashx?meth=GetResultByLongPolling&key={longPollingKey}&_={GetCurrentTime()}"
- );
- //Thread.Sleep(5000);
- //Console.WriteLine(Environment.TickCount);
- response = this.DownloadString(
- $"https://{host}/beanfun_block/generic_handlers/get_webstart_otp.ashx?SN={longPollingKey}&WebToken={this.WebToken}&SecretCode={secretCode}&ppppp=1F552AEAFF976018F942B13690C990F60ED01510DDF89165F1658CCE7BC21DBA&ServiceCode={service_code}&ServiceRegion={service_region}&ServiceAccount={acc.sid}&CreateTime={acc.screatetime.Replace(" ", "%20")}&d={Environment.TickCount}"
- );
- if (response == null || response == "")
- {
- this.errmsg = "OTPNoResponse";
- return null;
- }
- string[] responses = response.Split(';');
- if (responses.Length < 2)
- {
- this.errmsg = "OTPNoResponse";
- return null;
- }
- response = responses[1];
- if (responses[0] != "1")
- {
- this.errmsg =
- (
- System.Windows.Application.Current.TryFindResource("GetOtpError")
- as string
- )
- + "\r\n"
- + response;
- return null;
- }
- string key = response.Substring(0, 8);
- string plain = response.Substring(8);
- string otp = WCDESComp.DecryStrHex(plain, key);
- if (otp != null)
- {
- otp = otp.Trim('\0');
- this.errmsg = null;
- }
- else
- {
- this.errmsg = "DecryptOTPError";
- }
-
- return otp;
- }
- catch (Exception e)
- {
- this.errmsg =
- (System.Windows.Application.Current.TryFindResource("GetOtpError") as string)
- + "\n\n"
- + e.Message
- + "\n"
- + e.StackTrace;
- return null;
- }
- }
- }
-}
diff --git a/Beanfun/Tools/BeanfunClient.Verify.cs b/Beanfun/Tools/BeanfunClient.Verify.cs
deleted file mode 100644
index 94241d9..0000000
--- a/Beanfun/Tools/BeanfunClient.Verify.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.Drawing;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Windows.Media.Imaging;
-
-namespace Beanfun
-{
- public partial class BeanfunClient : WebClient
- {
- // Stored from getVerifyPageInfo HTML parsing
- public string verifyFormAction;
- public string verifyViewStateGenerator;
-
- public string getVerifyPageInfo()
- {
- try
- {
- string url = !string.IsNullOrEmpty(this.advanceCheckUrl)
- ? this.advanceCheckUrl
- : "https://tw.newlogin.beanfun.com/LoginCheck/AdvanceCheck.aspx";
- return this.DownloadString(url);
- }
- catch (Exception e)
- {
- this.errmsg = "VerifyUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- return null;
- }
- }
-
- public BitmapImage getVerifyCaptcha(string samplecaptcha)
- {
- if (string.IsNullOrWhiteSpace(samplecaptcha))
- return null;
-
- BitmapImage result;
- try
- {
- string url =
- "https://tw.newlogin.beanfun.com/LoginCheck/BotDetectCaptcha.ashx?get=image&c=c_logincheck_advancecheck_samplecaptcha&t="
- + samplecaptcha;
- byte[] buffer = this.DownloadData(url);
-
- if (buffer == null || buffer.Length < 500)
- {
- Debug.WriteLine("[Captcha] Downloaded data is too small to be an image.");
- return null;
- }
-
- result = new BitmapImage();
- result.BeginInit();
- result.CacheOption = BitmapCacheOption.OnLoad;
- result.StreamSource = new MemoryStream(buffer);
- result.EndInit();
- result.Freeze();
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"[Captcha] Error parsing image: {ex.Message}");
- result = null;
- }
- return result;
- }
-
- public string verify(
- string viewstate,
- string eventvalidation,
- string samplecaptcha,
- string verifyCode,
- string captchaCode
- )
- {
- try
- {
- NameValueCollection payload = new NameValueCollection();
- payload.Add("__VIEWSTATE", viewstate);
- if (!string.IsNullOrEmpty(this.verifyViewStateGenerator))
- payload.Add("__VIEWSTATEGENERATOR", this.verifyViewStateGenerator);
- payload.Add("__EVENTVALIDATION", eventvalidation);
- payload.Add("txtVerify", verifyCode);
- payload.Add("CodeTextBox", captchaCode);
- payload.Add("imgbtnSubmit.x", "19");
- payload.Add("imgbtnSubmit.y", "23");
- payload.Add("LBD_VCID_c_logincheck_advancecheck_samplecaptcha", samplecaptcha);
-
- string submitUrl = !string.IsNullOrEmpty(this.verifyFormAction)
- ? this.verifyFormAction
- : "https://tw.newlogin.beanfun.com/LoginCheck/AdvanceCheck.aspx";
- return this.UploadString(submitUrl, payload);
- }
- catch (Exception e)
- {
- this.errmsg = "VerifyUnknown\n\n" + e.Message + "\n" + e.StackTrace;
- return null;
- }
- }
- }
-}
diff --git a/Beanfun/Tools/BeanfunClient.cs b/Beanfun/Tools/BeanfunClient.cs
deleted file mode 100644
index 50e5f97..0000000
--- a/Beanfun/Tools/BeanfunClient.cs
+++ /dev/null
@@ -1,261 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Text;
-
-namespace Beanfun
-{
- public partial class BeanfunClient : WebClient
- {
- private System.Net.CookieContainer CookieContainer;
- private Uri ResponseUri;
- public string errmsg;
- private string webtoken;
- public List accountList;
- public int remainPoint = 0;
- public string accountAmountLimitNotice;
- bool redirect;
- private const string userAgent =
- "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36";
- private string LoginToken;
- private string SessionKey;
- private string totpResponse;
- private string totpUrl;
- public string advanceCheckUrl;
-
- public int Timeout { get; set; }
-
- public string WebToken
- {
- get { return webtoken; }
- }
-
- public BeanfunClient()
- {
- this.redirect = true;
- this.CookieContainer = new System.Net.CookieContainer();
- this.Headers.Set("User-Agent", userAgent);
- this.Headers.Set("Accept-Encoding", "identity");
- this.Encoding = Encoding.UTF8;
- this.ResponseUri = null;
- this.errmsg = null;
- this.webtoken = null;
- this.accountList = new List();
- this.accountAmountLimitNotice = "";
- this.Timeout = 30 * 1000;
- }
-
- public string DownloadString(string Uri, Encoding Encoding)
- {
- this.Headers.Set("User-Agent", userAgent);
- this.Headers.Set("Accept-Encoding", "identity");
- var ret = (Encoding.GetString(base.DownloadData(Uri)));
- return ret;
- }
-
- public new string DownloadString(string Uri)
- {
- this.Headers.Set("User-Agent", userAgent);
- this.Headers.Set("Accept-Encoding", "identity");
- var ret = base.DownloadString(Uri);
- return ret;
- }
-
- public string UploadString(string skey, NameValueCollection payload)
- {
- this.Headers.Set("User-Agent", userAgent);
- this.Headers.Set("Accept-Encoding", "identity");
- return Encoding.UTF8.GetString(base.UploadValues(skey, payload));
- }
-
- public string UploadStringGZip(string skey, NameValueCollection payload)
- {
- this.Headers.Set("User-Agent", userAgent);
- this.Headers.Set("Accept-Encoding", "gzip, deflate, br");
- byte[] byteArray = base.UploadValues(skey, payload);
- if (byteArray.Length <= 0)
- return "";
- if (byteArray[0] == 0x1F && byteArray[1] == 0x8B)
- {
- MemoryStream ms = new MemoryStream(byteArray);
- MemoryStream msTemp = new MemoryStream();
- int count = 0;
- System.IO.Compression.GZipStream gzip = new System.IO.Compression.GZipStream(
- ms,
- System.IO.Compression.CompressionMode.Decompress
- );
- byte[] buf = new byte[1000];
-
- while ((count = gzip.Read(buf, 0, buf.Length)) > 0)
- {
- msTemp.Write(buf, 0, count);
- }
- byteArray = msTemp.ToArray();
- }
- return Encoding.UTF8.GetString(byteArray);
- }
-
- protected override WebRequest GetWebRequest(Uri address)
- {
- ServicePointManager.SecurityProtocol =
- SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;
- HttpWebRequest webRequest = base.GetWebRequest(address) as HttpWebRequest;
-
- if (webRequest != null)
- {
- webRequest.Timeout = this.Timeout;
- if (this.CookieContainer == null)
- this.CookieContainer = new CookieContainer();
-
- webRequest.CookieContainer = this.CookieContainer;
- webRequest.AllowAutoRedirect = this.redirect;
- webRequest.AutomaticDecompression =
- DecompressionMethods.GZip | DecompressionMethods.Deflate;
- }
- return webRequest;
- }
-
- protected override WebResponse GetWebResponse(WebRequest request)
- {
- try
- {
- WebResponse webResponse = base.GetWebResponse(request);
- this.ResponseUri = webResponse.ResponseUri;
- return webResponse;
- }
- catch (WebException wex)
- {
- if (wex.Response != null)
- {
- var httpResponse = wex.Response as HttpWebResponse;
- Debug.WriteLine(
- $"[WebResponse] Status={httpResponse?.StatusCode} Uri={wex.Response.ResponseUri}"
- );
- this.ResponseUri = wex.Response.ResponseUri;
- return wex.Response;
- }
- throw;
- }
- }
-
- public CookieCollection GetCookies()
- {
- return this.CookieContainer.GetCookies(
- new Uri(
- "https://" + (App.LoginRegion == "TW" ? "tw" : "bfweb.hk") + ".beanfun.com/"
- )
- );
- }
-
- private string GetCookie(string cookieName)
- {
- foreach (Cookie cookie in GetCookies())
- {
- if (cookie.Name == cookieName)
- {
- return cookie.Value;
- }
- }
- return null;
- }
-
- public void SetCookie(string name, string value, string domain, string path = "/")
- {
- this.CookieContainer.Add(new Cookie(name, value, path, domain));
- }
-
- public void SetWebToken(string token)
- {
- this.webtoken = token;
- }
-
- private string GetCurrentTime(int method = 0)
- {
- DateTime date = DateTime.Now;
- switch (method)
- {
- case 1:
- return (date.Year - 1900).ToString()
- + (date.Month - 1).ToString()
- + date.ToString("ddHHmmssfff");
- case 2:
- return date.Year.ToString()
- + (date.Month - 1).ToString()
- + date.ToString("ddHHmmssfff");
- default:
- return date.ToString("yyyyMMddHHmmss.fff");
- }
- }
-
- public void Ping()
- {
- try
- {
- string url = "https://";
- if (App.LoginRegion == "TW")
- url += "tw";
- else
- url += "bfweb.hk";
- string ret = Encoding.GetString(
- this.DownloadData(
- url
- + ".beanfun.com/beanfun_block/generic_handlers/echo_token.ashx?webtoken=1"
- )
- );
-
- Console.WriteLine(GetCurrentTime() + " @ " + ret);
- }
- catch { }
- }
-
- public int getRemainPoint()
- {
- string response = null;
- System.Text.RegularExpressions.Regex regex;
-
- string url = "https://";
- if (App.LoginRegion == "TW")
- url += "tw";
- else
- url += "bfweb.hk";
- response = this.DownloadString(
- url +=
- ".beanfun.com/beanfun_block/generic_handlers/get_remain_point.ashx?webtoken=1"
- );
-
- try
- {
- regex = new System.Text.RegularExpressions.Regex("\"RemainPoint\" : \"(.*)\" }");
- if (regex.IsMatch(response))
- return int.Parse(regex.Match(response).Groups[1].Value);
- else
- return 0;
- }
- catch
- {
- return 0;
- }
- }
-
- public string getEmail()
- {
- if (App.LoginRegion == "HK")
- return "";
-
- this.Headers.Set("Referer", @"https://tw.beanfun.com/");
- string response = this.DownloadString(
- "https://tw.beanfun.com/beanfun_block/loader.ashx?service_code=999999&service_region=T0"
- );
- System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex(
- "BeanFunBlock.LoggedInUserData.Email = \"(.*)\";BeanFunBlock.LoggedInUserData.MessageCount"
- );
- if (regex.IsMatch(response))
- return regex.Match(response).Groups[1].Value;
- else
- return "";
- }
- }
-}
diff --git a/Beanfun/Update/ApplicationUpdater.cs b/Beanfun/Update/ApplicationUpdater.cs
deleted file mode 100644
index bdff783..0000000
--- a/Beanfun/Update/ApplicationUpdater.cs
+++ /dev/null
@@ -1,294 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Windows;
-using Newtonsoft.Json;
-
-namespace Beanfun.Update
-{
- class ApplicationUpdater
- {
- private static readonly string[] GH_PROXIES = new[]
- {
- "https://ghproxy.vip/",
- "https://ghproxy.net/",
- "https://ghfast.top/",
- };
-
- private const int ProbeTimeoutMs = 5000;
-
- private static readonly Lazy _cachedProxy = new Lazy(
- DiscoverProxy,
- LazyThreadSafetyMode.ExecutionAndPublication
- );
-
- private static bool TryProbe(string url)
- {
- try
- {
- var req = WebRequest.CreateHttp(url);
- req.Method = "HEAD";
- req.Timeout = ProbeTimeoutMs;
- req.UserAgent = $"Beanfun(V{App.AssemblyVersion})";
- using (req.GetResponse()) { }
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- private static string DiscoverProxy()
- {
- // Test direct GitHub access first
- if (TryProbe("https://api.github.com"))
- return "";
-
- // Direct access failed, try proxies
- foreach (var proxy in GH_PROXIES)
- {
- if (TryProbe(proxy + "https://api.github.com"))
- return proxy;
- }
-
- return "";
- }
-
- private static string GetProxy() => _cachedProxy.Value;
-
- public class GitHubRelease
- {
- [JsonProperty("name")]
- public string Name { get; set; }
-
- [JsonProperty("tag_name")]
- public string TagName { get; set; }
-
- [JsonProperty("prerelease")]
- public bool Prerelease { get; set; }
-
- [JsonProperty("body")]
- public string Body { get; set; }
-
- [JsonProperty("assets")]
- public List Assets { get; set; }
- }
-
- public class GitHubAsset
- {
- [JsonProperty("browser_download_url")]
- public string BrowserDownloadUrl { get; set; }
- }
-
- private static int _checkRunning;
-
- internal static void CheckApplicationUpdate(bool show)
- {
- // Prevent concurrent checks (e.g. startup probe + About-page click collision).
- if (Interlocked.CompareExchange(ref _checkRunning, 1, 0) != 0)
- return;
-
- var thread = new Thread(() =>
- {
- try
- {
- RunCheck(show);
- }
- finally
- {
- Interlocked.Exchange(ref _checkRunning, 0);
- }
- })
- {
- IsBackground = true,
- Name = "UpdateCheck",
- };
- thread.Start();
- }
-
- private static void RunCheck(bool show)
- {
- string proxy = GetProxy();
- var url = proxy + "https://api.github.com/repos/pungin/beanfun/releases";
-
- try
- {
- using (var client = new WebClient())
- {
- client.Headers.Add("User-Agent", $"Beanfun(V{App.AssemblyVersion})");
- client.Headers.Add("Accept", "application/vnd.github.v3+json");
- var json = Encoding.UTF8.GetString(client.DownloadData(url));
-
- var releases = JsonConvert.DeserializeObject>(json);
- GitHubRelease release = GetLastRelease(releases);
-
- if (release == null)
- return;
-
- // 1. 解析遠端 Tag (格式: vMajor.Minor.Patch.Timestamp)
- // Groups: [1]=Major, [2]=Minor, [3]=Patch, [4]=Timestamp
- var match = Regex.Match(release.TagName, @"^v(\d+)\.(\d+)\.(\d+)\.(\d+)$");
- if (!match.Success)
- return;
-
- string major = match.Groups[1].Value;
- string minor = match.Groups[2].Value;
- string patch = match.Groups[3].Value;
- string timestamp = match.Groups[4].Value;
-
- // 2. 準備顯示文字: 5.8.3(2604011114)
- string newVerDisplay = $"{major}.{minor}.{patch}({timestamp})";
-
- // 3. 數值比較邏輯 (傳入 patch 以支援 5.8.9 < 5.8.10)
- if (IsNewerVersion(App.AssemblyVersion, major, minor, patch, timestamp))
- {
- string msg = string.Format(
- Regex.Unescape(
- Application.Current.TryFindResource("NewVersionDetected") as string
- ?? "Detect New Version {0} (Current: {1})\n\n{2}"
- ),
- newVerDisplay,
- App.AssemblyVersion,
- release.Body
- );
-
- MessageBoxResult result = MessageBox.Show(
- msg,
- Application.Current.TryFindResource("UpdateCheck") as string
- ?? "Update Check",
- MessageBoxButton.OKCancel
- );
-
- if (result == MessageBoxResult.OK)
- {
- string downloadUrl =
- (release.Assets != null && release.Assets.Count > 0)
- ? proxy + release.Assets[0].BrowserDownloadUrl
- : $"https://github.com/pungin/Beanfun/releases/tag/{release.TagName}";
-
- Process.Start(
- new ProcessStartInfo
- {
- FileName = downloadUrl,
- UseShellExecute = true,
- }
- );
- }
- }
- else if (show)
- {
- MessageBox.Show(
- Application.Current.TryFindResource("NoUpdatesDetected") as string
- ?? "No Updates Found",
- Application.Current.TryFindResource("UpdateCheck") as string
- ?? "Update Check",
- MessageBoxButton.OK
- );
- }
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine("Update check failed: " + ex.Message);
- }
- }
-
- private static GitHubRelease GetLastRelease(List releases)
- {
- string channel = ConfigAppSettings.GetValue("updateChannel", "Stable");
- bool isBeta = channel.Equals("Beta") || channel.Equals("Preview");
-
- foreach (var release in releases)
- {
- if (isBeta)
- return release;
- if (!release.Prerelease)
- return release;
- }
- return null;
- }
-
- ///
- /// 比較版本號。將 Major, Minor, Patch 全部補齊 3 位後與 Timestamp 拼接進行 Long 比較。
- /// 確保 5.8.9 < 5.8.10 且 Timestamp 格式永遠大於舊版。
- ///
- private static bool IsNewerVersion(
- string localVer,
- string major,
- string minor,
- string patch,
- string timestamp
- )
- {
- try
- {
- // 提取本地 Timestamp
- var match = Regex.Match(localVer, @"(\d+)\.(\d+)\.?(\d+)?\.?\((\d+)\)");
-
- if (match.Success)
- {
- string localTimestamp = match.Groups[4].Value;
- if (timestamp == localTimestamp)
- {
- return false;
- }
-
- long remoteNum = long.Parse(
- string.Format(
- "{0:D3}{1:D3}{2:D3}{3}",
- int.Parse(major),
- int.Parse(minor),
- int.Parse(patch),
- timestamp
- )
- );
-
- int lMajor = int.Parse(match.Groups[1].Value);
- int lMinor = int.Parse(match.Groups[2].Value);
- int lPatch = string.IsNullOrEmpty(match.Groups[3].Value)
- ? 0
- : int.Parse(match.Groups[3].Value);
-
- long localNum = long.Parse(
- string.Format(
- "{0:D3}{1:D3}{2:D3}{3}",
- lMajor,
- lMinor,
- lPatch,
- localTimestamp
- )
- );
-
- return remoteNum > localNum;
- }
- else
- {
- long remoteNum = long.Parse(
- string.Format(
- "{0:D3}{1:D3}{2:D3}{3}",
- int.Parse(major),
- int.Parse(minor),
- int.Parse(patch),
- timestamp
- )
- );
-
- string digits = Regex.Replace(localVer, @"[^\d]", "");
- long localNum = long.Parse(digits.PadLeft(19, '0'));
-
- return remoteNum > localNum;
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine("Version comparison failed: " + ex.Message);
- return false;
- }
- }
- }
-}
diff --git a/Beanfun/WebView2Loader.dll b/Beanfun/WebView2Loader.dll
deleted file mode 100644
index 789ca3b..0000000
Binary files a/Beanfun/WebView2Loader.dll and /dev/null differ
diff --git a/Beanfun/Windows/AccRecovery.xaml b/Beanfun/Windows/AccRecovery.xaml
deleted file mode 100644
index 1311051..0000000
--- a/Beanfun/Windows/AccRecovery.xaml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/AccRecovery.xaml.cs b/Beanfun/Windows/AccRecovery.xaml.cs
deleted file mode 100644
index 323e844..0000000
--- a/Beanfun/Windows/AccRecovery.xaml.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Security.Cryptography;
-using System.Text;
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// AccRecovery.xaml 的交互逻辑
- ///
- public partial class AccRecovery : Window
- {
- private AccountManager accMan;
-
- public AccRecovery(AccountManager a)
- {
- InitializeComponent();
-
- accMan = a;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Export_Button_Click(object sender, RoutedEventArgs e)
- {
- string plaintext = accMan.exportRecord();
- byte[] plain_bytes = Encoding.UTF8.GetBytes(plaintext);
-
- using var md5 = MD5.Create();
- byte[] key = md5.ComputeHash(Encoding.UTF8.GetBytes(t_Password.Text));
- using var aes = Aes.Create();
- ICryptoTransform encrypt_AES = aes.CreateEncryptor(
- key,
- md5.ComputeHash(Encoding.UTF8.GetBytes("pungin"))
- );
-
- byte[] output = encrypt_AES.TransformFinalBlock(plain_bytes, 0, plain_bytes.Length);
- t_Data.Text = Convert.ToBase64String(output);
-
- MessageBox.Show(TryFindResource("ExportDone") as string);
- }
-
- private void Recovery_Button_Click(object sender, RoutedEventArgs e)
- {
- byte[] byte_pwd = Encoding.UTF8.GetBytes(t_Password.Text);
- using var md5 = MD5.Create();
- byte[] byte_pwdMD5 = md5.ComputeHash(byte_pwd);
-
- using var aes = Aes.Create();
- ICryptoTransform decrypt_AES = aes.CreateDecryptor(
- byte_pwdMD5,
- md5.ComputeHash(Encoding.UTF8.GetBytes("pungin"))
- );
-
- byte[] input = Convert.FromBase64String(t_Data.Text);
- try
- {
- byte[] byte_secretContent = decrypt_AES.TransformFinalBlock(input, 0, input.Length);
- string plaintext = Encoding.UTF8.GetString(byte_secretContent);
- if (false == accMan.importRecord(plaintext))
- {
- MessageBox.Show(TryFindResource("RecoveryFailed") as string);
- }
- else
- {
- MessageBox.Show(TryFindResource("RecoverySuccess") as string);
- App.MainWnd.loginMethodInit();
- }
- }
- catch
- {
- MessageBox.Show(TryFindResource("MsgDecryptFailed") as string);
- return;
- }
- }
- }
-}
diff --git a/Beanfun/Windows/AddAccount.xaml b/Beanfun/Windows/AddAccount.xaml
deleted file mode 100644
index 24ca8a0..0000000
--- a/Beanfun/Windows/AddAccount.xaml
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/AddAccount.xaml.cs b/Beanfun/Windows/AddAccount.xaml.cs
deleted file mode 100644
index 7a3581d..0000000
--- a/Beanfun/Windows/AddAccount.xaml.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// AddAccount.xaml 的交互逻辑
- ///
- public partial class AddAccount : Window
- {
- public AddAccount()
- {
- InitializeComponent();
- initPage();
- }
-
- private void initPage()
- {
- string s_Region = region.SelectedIndex == 0 ? "TW" : "HK";
- if (s_Region == "TW")
- t_Verify.Visibility = Visibility.Visible;
- else
- {
- t_Verify.Visibility = Visibility.Collapsed;
- t_Verify.Text = "";
- }
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void region_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (t_Verify != null)
- initPage();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (t_AccountID.Text == null || t_AccountID.Text == "")
- {
- MessageBox.Show(TryFindResource("AccountNeed") as string);
- return;
- }
- App.MainWnd.accountManager.addAccount(
- region.SelectedIndex == 0 ? "TW" : "HK",
- t_AccountID.Text,
- t_AccountName.Text,
- t_Password.Text,
- t_Verify.Text,
- 0,
- t_Password.Text == "" ? false : (bool)autoLogin.IsChecked
- );
- App.MainWnd.loginMethodInit();
- this.Close();
- }
- }
-}
diff --git a/Beanfun/Windows/AddServiceAccount.xaml b/Beanfun/Windows/AddServiceAccount.xaml
deleted file mode 100644
index e16b8b3..0000000
--- a/Beanfun/Windows/AddServiceAccount.xaml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/AddServiceAccount.xaml.cs b/Beanfun/Windows/AddServiceAccount.xaml.cs
deleted file mode 100644
index 9d53e05..0000000
--- a/Beanfun/Windows/AddServiceAccount.xaml.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// AddServiceAccount.xaml 的交互逻辑
- ///
- public partial class AddServiceAccount : Window
- {
- public AddServiceAccount()
- {
- InitializeComponent();
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void ButtonOk_Click(object sender, RoutedEventArgs e)
- {
- if (
- txtNewServiceAccountDisplayName.Text == null
- || txtNewServiceAccountDisplayName.Text == ""
- )
- {
- MessageBox.Show(
- TryFindResource("MsgDisplayNameNeed") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- else if (!(bool)cbContract.IsChecked)
- {
- MessageBox.Show(
- TryFindResource("MsgTermsOfServiceNeed") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- this.Close();
- if (!App.MainWnd.AddServiceAccount(txtNewServiceAccountDisplayName.Text))
- {
- MessageBox.Show(
- TryFindResource("MsgCreateServiceAccountFailed") as string,
- TryFindResource("SystemInfo") as string
- );
- }
- }
-
- private void ButtonCancel_Click(object sender, RoutedEventArgs e)
- {
- this.Close();
- }
-
- private void aContract_Click(object sender, RoutedEventArgs e)
- {
- string contract = App.MainWnd.GetServiceContract();
- if (contract == "")
- {
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- }
- else
- {
- new Contract(contract).ShowDialog();
- }
- }
- }
-}
diff --git a/Beanfun/Windows/CaptchaWnd.xaml b/Beanfun/Windows/CaptchaWnd.xaml
deleted file mode 100644
index 24a1168..0000000
--- a/Beanfun/Windows/CaptchaWnd.xaml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/CaptchaWnd.xaml.cs b/Beanfun/Windows/CaptchaWnd.xaml.cs
deleted file mode 100644
index 99fbed6..0000000
--- a/Beanfun/Windows/CaptchaWnd.xaml.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using System.IO;
-using System.Windows;
-using System.Windows.Input;
-using System.Windows.Media.Imaging;
-
-namespace Beanfun
-{
- ///
- /// CaptchaWnd.xaml 的互動邏輯
- ///
- public partial class CaptchaWnd : Window
- {
- public string Captcha
- {
- get { return CodeTextBox.Text; }
- }
- private string samplecaptcha;
- private BeanfunClient Client;
-
- public CaptchaWnd(BeanfunClient client, string samplecaptcha)
- {
- InitializeComponent();
- this.samplecaptcha = samplecaptcha;
- Client = client;
- Button_Click(null, null);
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- BitmapImage result;
- try
- {
- byte[] buffer = Client.DownloadData(
- "https://tw.newlogin.beanfun.com/login/BotDetectCaptcha.ashx?get=image&c=c_login_idpass_form_samplecaptcha&t="
- + samplecaptcha
- );
- result = new BitmapImage();
- result.BeginInit();
- result.StreamSource = new MemoryStream(buffer);
- result.EndInit();
- }
- catch (Exception)
- {
- MessageBox.Show(TryFindResource("LoadCaptchaFailed") as string);
- return;
- }
- c_login_idpass_form_samplecaptcha_CaptchaImage.Source = result;
- }
-
- private void Button_Click_1(object sender, RoutedEventArgs e)
- {
- this.Close();
- }
- }
-}
diff --git a/Beanfun/Windows/ChangeAccount.xaml b/Beanfun/Windows/ChangeAccount.xaml
deleted file mode 100644
index 58ec6c2..0000000
--- a/Beanfun/Windows/ChangeAccount.xaml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/ChangeAccount.xaml.cs b/Beanfun/Windows/ChangeAccount.xaml.cs
deleted file mode 100644
index a1f9b50..0000000
--- a/Beanfun/Windows/ChangeAccount.xaml.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// ChangeAccount.xaml 的互動邏輯
- ///
- public partial class ChangeAccount : Window
- {
- string region,
- account;
- int changedIndex;
-
- public ChangeAccount(int changedIndex, string region, string account)
- {
- this.region = region;
- this.account = account;
- this.changedIndex = changedIndex;
- InitializeComponent();
- t_AccountID.Text = account;
- t_AccountName.Text = App.MainWnd.accountManager.getNameByAccount(region, account);
- autoLogin.IsChecked = App.MainWnd.accountManager.getAutoLoginByAccount(region, account);
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- if (t_AccountID.Text == null || t_AccountID.Text == "")
- {
- MessageBox.Show(TryFindResource("AccountNeed") as string);
- return;
- }
- string pwd = App.MainWnd.accountManager.getPasswordByAccount(region, account);
- string verify = App.MainWnd.accountManager.getVerifyByAccount(region, account);
- int method = App.MainWnd.accountManager.getMethodByAccount(region, account);
- App.MainWnd.accountManager.removeAccount(region, account);
- App.MainWnd.accountManager.addAccount(
- changedIndex,
- region,
- t_AccountID.Text,
- t_AccountName.Text,
- pwd,
- verify,
- method,
- (bool)autoLogin.IsChecked
- );
- App.MainWnd.loginMethodInit();
- this.Close();
- }
- }
-}
diff --git a/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml b/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml
deleted file mode 100644
index d088365..0000000
--- a/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml.cs b/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml.cs
deleted file mode 100644
index fc43ecd..0000000
--- a/Beanfun/Windows/ChangeServiceAccountDisplayName.xaml.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// ChangeServiceAccountDisplayName.xaml 的交互逻辑
- ///
- public partial class ChangeServiceAccountDisplayName : Window
- {
- public ChangeServiceAccountDisplayName(string name)
- {
- InitializeComponent();
- txtNewServiceAccountDisplayName.Text = name;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void ButtonOk_Click(object sender, RoutedEventArgs e)
- {
- this.Close();
- if (!App.MainWnd.ChangeServiceAccountDisplayName(txtNewServiceAccountDisplayName.Text))
- {
- MessageBox.Show(
- TryFindResource("MsgChangeDisplayNameError") as string,
- TryFindResource("SystemInfo") as string
- );
- }
- }
-
- private void ButtonCancel_Click(object sender, RoutedEventArgs e)
- {
- this.Close();
- }
- }
-}
diff --git a/Beanfun/Windows/Contract.xaml b/Beanfun/Windows/Contract.xaml
deleted file mode 100644
index 6d5ad9d..0000000
--- a/Beanfun/Windows/Contract.xaml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
diff --git a/Beanfun/Windows/Contract.xaml.cs b/Beanfun/Windows/Contract.xaml.cs
deleted file mode 100644
index ec467dd..0000000
--- a/Beanfun/Windows/Contract.xaml.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
-
-namespace Beanfun
-{
- ///
- /// Contract.xaml 的交互逻辑
- ///
- public partial class Contract : Window
- {
- public Contract(string ct)
- {
- InitializeComponent();
- contract.Text = ct;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
- }
-}
diff --git a/Beanfun/Windows/CopyBox.xaml b/Beanfun/Windows/CopyBox.xaml
deleted file mode 100644
index ed39864..0000000
--- a/Beanfun/Windows/CopyBox.xaml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/CopyBox.xaml.cs b/Beanfun/Windows/CopyBox.xaml.cs
deleted file mode 100644
index 186eaf8..0000000
--- a/Beanfun/Windows/CopyBox.xaml.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// CopyBox.xaml 的交互逻辑
- ///
- public partial class CopyBox : Window
- {
- public CopyBox(string title, string value)
- {
- InitializeComponent();
- this.Title = title;
- t_Value.Text = value;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- WindowsAPI.CopyText(t_Value.Text);
- MessageBox.Show(TryFindResource("CopyFinished") as string);
- }
- catch
- {
- MessageBox.Show(TryFindResource("CopyFailed") as string);
- }
- }
- }
-}
diff --git a/Beanfun/Windows/CoreCalculator.xaml b/Beanfun/Windows/CoreCalculator.xaml
deleted file mode 100644
index e4f9fef..0000000
--- a/Beanfun/Windows/CoreCalculator.xaml
+++ /dev/null
@@ -1,132 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/CoreCalculator.xaml.cs b/Beanfun/Windows/CoreCalculator.xaml.cs
deleted file mode 100644
index a7aec29..0000000
--- a/Beanfun/Windows/CoreCalculator.xaml.cs
+++ /dev/null
@@ -1,308 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Windows;
-
-namespace Beanfun
-{
- ///
- /// CoreCalculator.xaml 的交互逻辑
- ///
- public partial class CoreCalculator : Window
- {
- public ObservableCollection MustSkills { get; private set; } =
- new ObservableCollection();
- private ObservableCollection _mainSkillSource = new ObservableCollection();
- private int _useOtherSkillCount = 0;
- public ObservableCollection MainSkillSource
- {
- get
- {
- if (_mainSkillSource.Count != MustSkills.Count + _useOtherSkillCount + 1)
- {
- _mainSkillSource = new ObservableCollection(MustSkills);
- for (int i = 0; i <= _useOtherSkillCount; i++)
- {
- _mainSkillSource.Add(
- string.Format("{0}{1}", TryFindResource("Others"), i + 1)
- );
- }
- }
- return _mainSkillSource;
- }
- }
- private ObservableCollection _secondarySkillSource =
- new ObservableCollection();
- public ObservableCollection SecondarySkillSource
- {
- get
- {
- if (_secondarySkillSource.Count != MustSkills.Count + 1)
- {
- _secondarySkillSource = new ObservableCollection(MustSkills);
- _secondarySkillSource.Add(TryFindResource("Others") as string);
- }
- return _secondarySkillSource;
- }
- }
- public ObservableCollection CoreItems { get; private set; } =
- new ObservableCollection();
-
- public CoreCalculator()
- {
- InitializeComponent();
- DataContext = this;
- }
-
- private void btn_AddMustSkill_Click(object sender, RoutedEventArgs e)
- {
- if (t_SkillName.Text == "")
- {
- this.errorMessage(TryFindResource("SkillNameIsEmpty") as string);
- return;
- }
- if (MustSkills.Contains(t_SkillName.Text))
- {
- this.errorMessage(TryFindResource("SkillNameIsRepeat") as string);
- return;
- }
- MustSkills.Add(t_SkillName.Text);
- c_Skill1.ItemsSource = MainSkillSource;
- c_Skill2.ItemsSource = SecondarySkillSource;
- c_Skill3.ItemsSource = SecondarySkillSource;
- btn_Calculator.Content = string.Format(
- TryFindResource("CaculatorCore") as string,
- this.mustCoreCount(),
- MustSkills.Count
- );
- t_SkillName.Text = "";
- }
-
- private void btn_AddCore_Click(object sender, RoutedEventArgs e)
- {
- if (c_Skill1.Text == "" || c_Skill2.Text == "" || c_Skill3.Text == "")
- {
- this.errorMessage(TryFindResource("CoreSkillNameIsEmpty") as string);
- return;
- }
- if (
- c_Skill1.Text == c_Skill2.Text
- || c_Skill1.Text == c_Skill3.Text
- || c_Skill2.Text == c_Skill3.Text
- && c_Skill2.Text != TryFindResource("Others") as string
- )
- {
- this.errorMessage(TryFindResource("CoreSkillNameIsRepeat") as string);
- return;
- }
- CoreItem item = new CoreItem(c_Skill1.Text, c_Skill2.Text, c_Skill3.Text);
- if (CoreItems.Contains(item))
- {
- this.errorMessage(TryFindResource("CoreIsRepeat") as string);
- return;
- }
- CoreItems.Add(item);
- if (
- item.skill1
- == string.Format("{0}{1}", TryFindResource("Others"), _useOtherSkillCount + 1)
- )
- {
- _useOtherSkillCount++;
- c_Skill1.ItemsSource = MainSkillSource;
- }
- }
-
- private void btn_DeleteCore_Click(object sender, RoutedEventArgs e)
- {
- if (l_Cores.SelectedItem is CoreItem)
- {
- CoreItems.Remove((CoreItem)l_Cores.SelectedItem);
- }
- }
-
- private void btn_Calculator_Click(object sender, RoutedEventArgs e)
- {
- btn_Calculator.IsEnabled = false;
- int must_count = this.mustCoreCount();
- List> result = new List>();
- int size = CoreItems.Count;
- bool[] zero = new bool[size];
- for (int i = 0; i < size; i++)
- {
- zero[i] = i < must_count;
- }
- for (; ; )
- {
- List sub = new List();
- Dictionary keys = new Dictionary();
- int index = -1;
- bool per1 = false;
- int leftCount = -1;
- for (int i = 0; i < size; i++)
- {
- if (zero[i])
- {
- CoreItem item = CoreItems[i];
- if (!keys.ContainsKey(item.skill1))
- {
- keys[item.skill1] = true;
- sub.Add(item);
- }
- }
- if (index == -1)
- {
- if (per1 && !zero[i])
- {
- zero[i] = true;
- index = i;
- }
- else
- {
- per1 = zero[i];
- if (per1)
- leftCount++;
- }
- }
- }
- for (int i = 0; i < index; i++)
- {
- zero[i] = i < leftCount;
- }
- if (sub.Count == must_count)
- {
- Dictionary temp = new Dictionary();
- foreach (string skill in MustSkills)
- {
- foreach (CoreItem item in sub)
- {
- if (
- skill == item.skill1
- || skill == item.skill2
- || skill == item.skill3
- )
- {
- temp[skill] = temp.ContainsKey(skill) ? temp[skill] + 1 : 1;
- }
- }
- }
- bool isPerfect = true;
- foreach (string skill in MustSkills)
- {
- if (!temp.ContainsKey(skill) || temp[skill] < 2)
- {
- isPerfect = false;
- break;
- }
- }
- if (isPerfect)
- result.Add(sub);
- }
-
- if (index == -1)
- break;
- }
- if (result.Count <= 0)
- {
- t_Result.Text = (TryFindResource("NotFindPerfectCore") as string) + "\r\nBy:LinTx";
- }
- else
- {
- string r = "";
- for (int i = 0; i < result.Count; i++)
- {
- List items = result[i];
- r += string.Format(TryFindResource("CoreGroup") as string, i + 1) + "\r\n";
- foreach (CoreItem item in items)
- {
- r = r + item.ToString() + "\r\n";
- }
- r = r + "\r\n";
- }
- r = r + "By:LinTx";
- t_Result.Text = r;
- }
- btn_Calculator.IsEnabled = true;
- }
-
- private int mustCoreCount()
- {
- int count = (int)Math.Ceiling((double)MustSkills.Count * 2 / 3);
- if (count < 2)
- count = 2;
- return count;
- }
-
- private void errorMessage(string message)
- {
- MessageBox.Show(
- message,
- TryFindResource("SystemInfo") as string,
- MessageBoxButton.OK,
- MessageBoxImage.Error
- );
- }
-
- private void btn_DeleteMustSkill_Click(object sender, RoutedEventArgs e)
- {
- if (l_MustSkills.SelectedItem is string)
- {
- MustSkills.Remove((string)l_MustSkills.SelectedItem);
- c_Skill1.ItemsSource = MainSkillSource;
- c_Skill2.ItemsSource = SecondarySkillSource;
- c_Skill3.ItemsSource = SecondarySkillSource;
- btn_Calculator.Content = string.Format(
- TryFindResource("CaculatorCore") as string,
- this.mustCoreCount(),
- MustSkills.Count
- );
- }
- }
- }
-
- public class CoreItem : IEquatable
- {
- public string skill1 { get; }
- public string skill2 { get; }
- public string skill3 { get; }
-
- public CoreItem(string skill1, string skill2, string skill3)
- {
- this.skill1 = skill1;
- this.skill2 = skill2;
- this.skill3 = skill3;
- }
-
- public override int GetHashCode()
- {
- int hash1 = skill1.GetHashCode();
- int hash2 = skill2.GetHashCode();
- int hash3 = skill3.GetHashCode();
- return hash1 + hash2 + hash3;
- }
-
- public override string ToString()
- {
- return string.Format(
- "{0}({1})/{2}/{3}",
- this.skill1,
- Application.Current.TryFindResource("Main"),
- this.skill2,
- this.skill3
- );
- }
-
- public bool Equals(CoreItem other)
- {
- return this.skill1 == other.skill1
- && (
- this.skill2 == other.skill2 && this.skill3 == other.skill3
- || this.skill2 == other.skill3 && this.skill3 == other.skill2
- );
- }
-
- public string[] Skills
- {
- get { return new string[] { this.skill1, this.skill2, this.skill3 }; }
- }
- }
-}
diff --git a/Beanfun/Windows/EquipCalculator.xaml b/Beanfun/Windows/EquipCalculator.xaml
deleted file mode 100644
index a4b8e13..0000000
--- a/Beanfun/Windows/EquipCalculator.xaml
+++ /dev/null
@@ -1,830 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/EquipCalculator.xaml.cs b/Beanfun/Windows/EquipCalculator.xaml.cs
deleted file mode 100644
index e9e8732..0000000
--- a/Beanfun/Windows/EquipCalculator.xaml.cs
+++ /dev/null
@@ -1,952 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Windows;
-
-namespace Beanfun
-{
- ///
- /// EquipCalculator.xaml 的交互逻辑
- ///
- public partial class EquipCalculator : Window
- {
- private class Scroll
- {
- public ScrollStat Weapon { get; set; }
- public ScrollStat Armor { get; set; }
- public ScrollStat Accessory { get; set; }
- }
-
- private class ScrollStat
- {
- public byte StatMin { get; set; }
- public byte StatMax { get; set; }
- public byte Stat
- {
- get
- {
- return RandomType == 0 ? StatMin
- : RandomType == 1 ? (byte)((StatMin + StatMax) / 2)
- : StatMax;
- }
- }
-
- public byte AtkMin { get; set; }
- public byte AtkMax { get; set; }
- public byte Atk
- {
- get
- {
- return RandomType == 0 ? AtkMin
- : RandomType == 1 ? (byte)((AtkMin + AtkMax) / 2)
- : AtkMax;
- }
- }
-
- public bool IsRandom
- {
- get { return StatMin == StatMax && AtkMin == AtkMax; }
- }
-
- public byte RandomType { get; set; }
-
- public ScrollStat(byte stat, byte atk)
- {
- StatMin = stat;
- StatMax = stat;
- AtkMin = atk;
- AtkMax = atk;
- }
-
- public ScrollStat(byte statMin, byte statMax, byte atkMin, byte atkMax)
- {
- StatMin = statMin;
- StatMax = statMax;
- AtkMin = atkMin;
- AtkMax = atkMax;
- }
- }
-
- static class Scrolls
- {
- public static Scroll Destiny = new Scroll();
- public static Scroll Glory = new Scroll();
- public static Scroll Black = new Scroll();
- public static Scroll V = new Scroll();
- public static Scroll X = new Scroll();
- public static Scroll Red = new Scroll();
- public static Scroll JD = new Scroll();
- public static Scroll SM = new Scroll();
- public static Scroll BM = new Scroll();
-
- static Scrolls()
- {
- Destiny.Weapon = new ScrollStat(14, 20, 14, 20);
- Destiny.Armor = new ScrollStat(0, 0, 9, 15);
- Destiny.Accessory = new ScrollStat(0, 0, 9, 15);
- Destiny.Weapon.RandomType = 1;
- Destiny.Accessory.RandomType = Destiny.Weapon.RandomType;
- Destiny.Armor.RandomType = Destiny.Weapon.RandomType;
-
- Glory.Weapon = new ScrollStat(10, 20, 10, 20);
- Glory.Armor = new ScrollStat(0, 0, 5, 15);
- Glory.Accessory = new ScrollStat(0, 0, 5, 15);
- Glory.Weapon.RandomType = 1;
- Glory.Accessory.RandomType = Glory.Weapon.RandomType;
- Glory.Armor.RandomType = Glory.Weapon.RandomType;
-
- Black.Weapon = new ScrollStat(14, 14);
- Black.Armor = new ScrollStat(2, 9);
- Black.Accessory = new ScrollStat(0, 9);
-
- V.Weapon = new ScrollStat(11, 13);
- V.Armor = new ScrollStat(0, 8);
- V.Accessory = new ScrollStat(0, 8);
-
- X.Weapon = new ScrollStat(10, 12);
- X.Armor = new ScrollStat(0, 7);
- X.Accessory = new ScrollStat(0, 7);
-
- Red.Weapon = new ScrollStat(8, 10);
- Red.Armor = new ScrollStat(0, 5);
- Red.Accessory = new ScrollStat(0, 5);
-
- JD.Weapon = new ScrollStat(5, 9);
- JD.Armor = new ScrollStat(0, 4);
- JD.Accessory = new ScrollStat(0, 4);
-
- SM.Weapon = new ScrollStat(5, 7);
- SM.Armor = new ScrollStat(5, 1);
- SM.Accessory = new ScrollStat(5, 1);
-
- BM.Weapon = new ScrollStat(4, 7);
- BM.Armor = new ScrollStat(5, 0);
- BM.Accessory = new ScrollStat(5, 0);
- }
- }
-
- bool InitFinish = false;
-
- public EquipCalculator()
- {
- InitializeComponent();
- InitFinish = true;
- }
-
- private void Window_MouseLeftButtonDown(
- object sender,
- System.Windows.Input.MouseButtonEventArgs e
- )
- {
- this.DragMove();
- }
-
- private void rb_EqpTyp_IsCheckedChanged(object sender, RoutedEventArgs e)
- {
- if (!InitFinish)
- return;
- cb_Superior.IsChecked = false;
- cb_Superior.Visibility =
- (bool)rb_Lv150.IsChecked && ((bool)rb_Glove.IsChecked || (bool)rb_Armor.IsChecked)
- ? Visibility.Visible
- : Visibility.Collapsed;
- lbl_HeartNotice.Visibility = (bool)rb_Heart.IsChecked
- ? Visibility.Visible
- : Visibility.Collapsed;
- calcStat();
- }
-
- private void rb_ReqLev_IsCheckedChanged(object sender, RoutedEventArgs e)
- {
- if (!InitFinish)
- return;
- if (
- !(bool)rb_Lv150.IsChecked
- || (!(bool)rb_Glove.IsChecked && !(bool)rb_Armor.IsChecked)
- )
- {
- cb_Superior.IsChecked = false;
- cb_Superior.Visibility = Visibility.Collapsed;
- }
- else
- cb_Superior.Visibility = Visibility.Visible;
- calcStat();
- }
-
- private void rb_DestinyType_IsCheckedChanged(object sender, RoutedEventArgs e)
- {
- if (!InitFinish)
- return;
- Scrolls.Destiny.Weapon.RandomType = (byte)(
- (bool)rb_DestinyMin.IsChecked ? 0
- : (bool)rb_DestinyAverage.IsChecked ? 1
- : 2
- );
- Scrolls.Destiny.Accessory.RandomType = Scrolls.Destiny.Weapon.RandomType;
- Scrolls.Destiny.Armor.RandomType = Scrolls.Destiny.Weapon.RandomType;
- calcStat();
- }
-
- private void rb_GloryType_IsCheckedChanged(object sender, RoutedEventArgs e)
- {
- if (!InitFinish)
- return;
- Scrolls.Glory.Weapon.RandomType = (byte)(
- (bool)rb_GloryMin.IsChecked ? 0
- : (bool)rb_GloryAverage.IsChecked ? 1
- : 2
- );
- Scrolls.Glory.Accessory.RandomType = Scrolls.Glory.Weapon.RandomType;
- Scrolls.Glory.Armor.RandomType = Scrolls.Glory.Weapon.RandomType;
- calcStat();
- }
-
- private void cb_Superior_IsCheckedChanged(object sender, RoutedEventArgs e)
- {
- if (!InitFinish)
- return;
- lbl_StarForceMax.Content = (bool)cb_Superior.IsChecked ? "15" : "25";
- if ((bool)cb_Superior.IsChecked)
- {
- rb_Lv160.Visibility = Visibility.Collapsed;
- rb_Lv200.Visibility = Visibility.Collapsed;
- if (!(bool)rb_Lv150.IsChecked)
- {
- rb_Lv150.IsChecked = true;
- return;
- }
- }
- else
- {
- rb_Lv160.Visibility = Visibility.Visible;
- rb_Lv200.Visibility = Visibility.Visible;
- }
- calcStat();
- }
-
- private void calcStat_TextChanged(
- object sender,
- System.Windows.Controls.TextChangedEventArgs e
- )
- {
- calcStat();
- }
-
- private void t_BaseStat_GotFocus(object sender, RoutedEventArgs e)
- {
- t_BaseStat.Text = "";
- }
-
- private void t_FlameStat_GotFocus(object sender, RoutedEventArgs e)
- {
- t_FlameStat.Text = "";
- }
-
- private void t_BaseATK_GotFocus(object sender, RoutedEventArgs e)
- {
- t_BaseATK.Text = "";
- }
-
- private void t_FlameATK_GotFocus(object sender, RoutedEventArgs e)
- {
- t_FlameATK.Text = "";
- }
-
- private void t_StarForce_GotFocus(object sender, RoutedEventArgs e)
- {
- t_StarForce.Text = "";
- }
-
- private void t_DestinyNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_DestinyNum.Text = "";
- }
-
- private void t_GloryNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_GloryNum.Text = "";
- }
-
- private void t_BlackNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_BlackNum.Text = "";
- }
-
- private void t_VNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_VNum.Text = "";
- }
-
- private void t_XNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_XNum.Text = "";
- }
-
- private void t_RedNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_RedNum.Text = "";
- }
-
- private void t_JDNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_JDNum.Text = "";
- }
-
- private void t_SMNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_SMNum.Text = "";
- }
-
- private void t_BMNum_GotFocus(object sender, RoutedEventArgs e)
- {
- t_BMNum.Text = "";
- }
-
- private void t_ScrollStat_GotFocus(object sender, RoutedEventArgs e)
- {
- t_ScrollStat.Text = "";
- }
-
- private void t_ScrollATK_GotFocus(object sender, RoutedEventArgs e)
- {
- t_ScrollATK.Text = "";
- }
-
- private void calcStat()
- {
- if (!InitFinish)
- return;
- byte eqpTyp = (byte)(
- (bool)rb_Weapon.IsChecked ? 0
- : (bool)rb_Glove.IsChecked ? 1
- : (bool)rb_Armor.IsChecked ? 2
- : (bool)rb_Accessory.IsChecked ? 3
- : 4
- );
- short reqLev = (short)(
- (bool)rb_Lv200.IsChecked ? 200
- : (bool)rb_Lv160.IsChecked ? 160
- : 150
- );
- bool superior =
- (bool)cb_Superior.IsChecked && cb_Superior.Visibility == Visibility.Visible;
-
- int baseStat;
- try
- {
- baseStat = int.Parse(t_BaseStat.Text);
- }
- catch
- {
- baseStat = 0;
- }
-
- byte flameStat;
- try
- {
- flameStat = byte.Parse(t_FlameStat.Text);
- }
- catch
- {
- flameStat = 0;
- }
-
- int baseATK;
- try
- {
- baseATK = int.Parse(t_BaseATK.Text);
- }
- catch
- {
- baseATK = 0;
- }
-
- byte flameATK;
- try
- {
- flameATK = byte.Parse(t_FlameATK.Text);
- }
- catch
- {
- flameATK = 0;
- }
-
- byte starForce;
- try
- {
- starForce = byte.Parse(t_StarForce.Text);
- }
- catch
- {
- starForce = 0;
- }
-
- byte destinyNum;
- try
- {
- destinyNum = byte.Parse(t_DestinyNum.Text);
- }
- catch
- {
- destinyNum = 0;
- }
-
- byte gloryNum;
- try
- {
- gloryNum = byte.Parse(t_GloryNum.Text);
- }
- catch
- {
- gloryNum = 0;
- }
-
- byte blackNum;
- try
- {
- blackNum = byte.Parse(t_BlackNum.Text);
- }
- catch
- {
- blackNum = 0;
- }
-
- byte vNum;
- try
- {
- vNum = byte.Parse(t_VNum.Text);
- }
- catch
- {
- vNum = 0;
- }
-
- byte xNum;
- try
- {
- xNum = byte.Parse(t_XNum.Text);
- }
- catch
- {
- xNum = 0;
- }
-
- byte redNum;
- try
- {
- redNum = byte.Parse(t_RedNum.Text);
- }
- catch
- {
- redNum = 0;
- }
-
- byte jdNum;
- try
- {
- jdNum = byte.Parse(t_JDNum.Text);
- }
- catch
- {
- jdNum = 0;
- }
-
- byte smNum;
- try
- {
- smNum = byte.Parse(t_SMNum.Text);
- }
- catch
- {
- smNum = 0;
- }
-
- byte bmNum;
- try
- {
- bmNum = byte.Parse(t_BMNum.Text);
- }
- catch
- {
- bmNum = 0;
- }
-
- int scrollStat;
- try
- {
- scrollStat = int.Parse(t_ScrollStat.Text);
- }
- catch
- {
- scrollStat = 0;
- }
-
- int scrollTK;
- try
- {
- scrollTK = int.Parse(t_ScrollATK.Text);
- }
- catch
- {
- scrollTK = 0;
- }
-
- int atk =
- baseATK
- + destinyNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Destiny.Weapon.Atk
- : (
- eqpTyp == 3
- ? Scrolls.Destiny.Accessory.Atk
- : Scrolls.Destiny.Armor.Atk
- )
- )
- + gloryNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Glory.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.Glory.Accessory.Atk : Scrolls.Glory.Armor.Atk)
- )
- + blackNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Black.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.Black.Accessory.Atk : Scrolls.Black.Armor.Atk)
- )
- + vNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.V.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.V.Accessory.Atk : Scrolls.V.Armor.Atk)
- )
- + xNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.X.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.X.Accessory.Atk : Scrolls.X.Armor.Atk)
- )
- + redNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Red.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.Red.Accessory.Atk : Scrolls.Red.Armor.Atk)
- )
- + jdNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.JD.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.JD.Accessory.Atk : Scrolls.JD.Armor.Atk)
- )
- + smNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.SM.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.SM.Accessory.Atk : Scrolls.SM.Armor.Atk)
- )
- + bmNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.BM.Weapon.Atk
- : (eqpTyp == 3 ? Scrolls.BM.Accessory.Atk : Scrolls.BM.Armor.Atk)
- )
- + scrollTK;
-
- int stat =
- baseStat
- + destinyNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Destiny.Weapon.Stat
- : (
- eqpTyp == 3
- ? Scrolls.Destiny.Accessory.Stat
- : Scrolls.Destiny.Armor.Stat
- )
- )
- + gloryNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Glory.Weapon.Stat
- : (
- eqpTyp == 3
- ? Scrolls.Glory.Accessory.Stat
- : Scrolls.Glory.Armor.Stat
- )
- )
- + blackNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Black.Weapon.Stat
- : (
- eqpTyp == 3
- ? Scrolls.Black.Accessory.Stat
- : Scrolls.Black.Armor.Stat
- )
- )
- + vNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.V.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.V.Accessory.Stat : Scrolls.V.Armor.Stat)
- )
- + xNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.X.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.X.Accessory.Stat : Scrolls.X.Armor.Stat)
- )
- + redNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.Red.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.Red.Accessory.Stat : Scrolls.Red.Armor.Stat)
- )
- + jdNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.JD.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.JD.Accessory.Stat : Scrolls.JD.Armor.Stat)
- )
- + smNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.SM.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.SM.Accessory.Stat : Scrolls.SM.Armor.Stat)
- )
- + bmNum
- * (
- eqpTyp == 0 || eqpTyp == 4
- ? Scrolls.BM.Weapon.Stat
- : (eqpTyp == 3 ? Scrolls.BM.Accessory.Stat : Scrolls.BM.Armor.Stat)
- )
- + scrollStat;
-
- Dictionary echantStats;
- for (byte i = 0; i < starForce; i++)
- {
- echantStats = getStarForceStats(superior, eqpTyp, i, atk, reqLev);
- stat += echantStats[1];
- atk += echantStats[2];
- }
-
- lbl_AddedStat.Content = stat - baseStat;
- lbl_TotalStat.Content = stat + flameStat;
- lbl_AddedATK.Content = atk - baseATK;
- lbl_TotalATK.Content = atk + flameATK;
- }
-
- private Dictionary getStarForceStats(
- bool superior,
- byte eqpTyp,
- byte starForce,
- int atk,
- short reqLev
- )
- {
- Dictionary stats = new Dictionary();
- stats.Add(1, 0);
- stats.Add(2, 0);
- if (superior)
- {
- // 尊貴裝
- switch (starForce)
- {
- case 0:
- stats.Remove(1);
- stats.Add(1, 19);
- break;
- case 1:
- stats.Remove(1);
- stats.Add(1, 20);
- break;
- case 2:
- stats.Remove(1);
- stats.Add(1, 22);
- break;
- case 3:
- stats.Remove(1);
- stats.Add(1, 25);
- break;
- case 4:
- stats.Remove(1);
- stats.Add(1, 29);
- break;
- case 5:
- case 6:
- case 7:
- case 8:
- case 9:
- stats.Remove(2);
- stats.Add(2, starForce + 4);
- break;
- case 10:
- case 11:
- case 12:
- case 13:
- case 14:
- stats.Remove(2);
- stats.Add(2, 15 + 2 * (starForce - 10));
- break;
- }
- }
- else if (eqpTyp == 0)
- {
- // 武器
-
- // 屬性
- int allStats;
- if (starForce >= 0 && starForce < 5)
- {
- allStats = 2;
- }
- else if (starForce >= 5 && starForce < 15)
- {
- allStats = 3;
- }
- else if (starForce < 22)
- {
- if (reqLev >= 200)
- allStats = 15;
- else if (reqLev >= 160)
- allStats = 13;
- else
- allStats = 11;
- }
- else
- {
- allStats = 0;
- }
- stats.Remove(1);
- stats.Add(1, allStats);
-
- // 攻擊力
- if (starForce < 15)
- {
- stats.Remove(2);
- stats.Add(2, (int)Math.Floor(atk / 50.0D) + 1);
- }
- else
- {
- int value = 0;
- switch (starForce)
- {
- case 15:
- if (reqLev >= 200)
- value = 13;
- else if (reqLev >= 160)
- value = 9;
- else
- value = 8;
- break;
- case 16:
- if (reqLev >= 200)
- value = 13;
- else
- value = 9;
- break;
- case 17:
- if (reqLev >= 200)
- value = 14;
- else if (reqLev >= 160)
- value = 10;
- else
- value = 9;
- break;
- case 18:
- if (reqLev >= 200)
- value = 14;
- else if (reqLev >= 160)
- value = 11;
- else
- value = 10;
- break;
- case 19:
- if (reqLev >= 200)
- value = 15;
- else if (reqLev >= 160)
- value = 12;
- else
- value = 11;
- break;
- case 20:
- if (reqLev >= 200)
- value = 16;
- else if (reqLev >= 160)
- value = 13;
- else
- value = 12;
- break;
- case 21:
- if (reqLev >= 200)
- value = 17;
- else if (reqLev >= 160)
- value = 14;
- else
- value = 13;
- break;
- case 22:
- if (reqLev >= 200)
- value = 34;
- else if (reqLev >= 160)
- value = 32;
- else
- value = 31;
- break;
- case 23:
- if (reqLev >= 200)
- value = 35;
- break;
- }
- stats.Remove(2);
- stats.Add(2, value);
- }
- }
- else
- {
- // 其他裝備
- int allStats;
- if (starForce >= 0 && starForce < 5)
- {
- allStats = 2;
- }
- else if (starForce >= 5 && starForce < 15)
- {
- allStats = 3;
- }
- else if (starForce < 22)
- {
- if (reqLev >= 200)
- allStats = 15;
- else if (reqLev >= 160)
- allStats = 13;
- else
- allStats = 11;
- }
- else
- {
- allStats = 0;
- }
- stats.Remove(1);
- stats.Add(1, allStats);
-
- if (starForce >= 15)
- {
- int value = 0;
- switch (starForce)
- {
- case 15:
- if (reqLev >= 200)
- value = 12;
- else if (reqLev >= 160)
- value = 10;
- else
- value = 9;
- break;
- case 16:
- if (reqLev >= 200)
- value = 13;
- else if (reqLev >= 160)
- value = 11;
- else
- value = 10;
- break;
- case 17:
- if (reqLev >= 200)
- value = 14;
- else if (reqLev >= 160)
- value = 12;
- else
- value = 11;
- break;
- case 18:
- if (reqLev >= 200)
- value = 15;
- else if (reqLev >= 160)
- value = 13;
- else
- value = 12;
- break;
- case 19:
- if (reqLev >= 200)
- value = 16;
- else if (reqLev >= 160)
- value = 14;
- else
- value = 13;
- break;
- case 20:
- if (reqLev >= 200)
- value = 17;
- else if (reqLev >= 160)
- value = 15;
- else
- value = 14;
- break;
- case 21:
- if (reqLev >= 200)
- value = 19;
- else if (reqLev >= 160)
- value = 17;
- else
- value = 16;
- break;
- case 22:
- if (reqLev >= 200)
- value = 21;
- else if (reqLev >= 160)
- value = 19;
- else
- value = 18;
- break;
- case 23:
- if (reqLev >= 200)
- value = 23;
- else if (reqLev >= 160)
- value = 21;
- else
- value = 20;
- break;
- case 24:
- if (reqLev >= 200)
- value = 25;
- else if (reqLev >= 160)
- value = 23;
- else
- value = 22;
- break;
- }
- stats.Remove(2);
- stats.Add(2, value);
- }
- else if (eqpTyp == 1)
- {
- int value = 0;
- switch (starForce)
- {
- case 4:
- case 6:
- case 8:
- case 10:
- case 12:
- value = 1;
- break;
- case 13:
- if (reqLev >= 200)
- value = 1;
- break;
- case 14:
- if (reqLev >= 200)
- value = 1;
- else
- value = 2;
- break;
- }
- stats.Remove(2);
- stats.Add(2, value);
- }
- }
- return stats;
- }
- }
-}
diff --git a/Beanfun/Windows/GameList.xaml b/Beanfun/Windows/GameList.xaml
deleted file mode 100644
index a62a737..0000000
--- a/Beanfun/Windows/GameList.xaml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/GameList.xaml.cs b/Beanfun/Windows/GameList.xaml.cs
deleted file mode 100644
index d567501..0000000
--- a/Beanfun/Windows/GameList.xaml.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media.Imaging;
-
-namespace Beanfun
-{
- ///
- /// GameList.xaml 的交互逻辑
- ///
- public partial class GameList : Window
- {
- public class Game
- {
- public Image image { get; set; }
- public string name { get; set; }
- public string service_code { get; set; }
- public string service_region { get; set; }
-
- public Game(BitmapImage source, string name, string service_code, string service_region)
- {
- this.image = new Image();
- image.Source = source;
- this.name = name;
- this.service_code = service_code;
- this.service_region = service_region;
- }
- }
-
- public GameList()
- {
- InitializeComponent();
-
- foreach (MainWindow.GameService game in App.MainWnd.GameList[App.LoginRegion.ToLower()])
- l_GameList.Items.Add(
- new Game(game.Large_image, game.name, game.service_code, game.service_region)
- );
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void l_GameList_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (l_GameList.SelectedIndex < 0)
- return;
- if (
- App.MainWnd.service_code != ((Game)l_GameList.SelectedItem).service_code
- || App.MainWnd.service_region != ((Game)l_GameList.SelectedItem).service_region
- )
- {
- App.MainWnd.service_code = ((Game)l_GameList.SelectedItem).service_code;
- App.MainWnd.service_region = ((Game)l_GameList.SelectedItem).service_region;
- App.MainWnd.selectedGameChanged();
- }
- this.Close();
- }
- }
-}
diff --git a/Beanfun/Windows/GamePassBrowser.xaml b/Beanfun/Windows/GamePassBrowser.xaml
deleted file mode 100644
index 2023f88..0000000
--- a/Beanfun/Windows/GamePassBrowser.xaml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
diff --git a/Beanfun/Windows/GamePassBrowser.xaml.cs b/Beanfun/Windows/GamePassBrowser.xaml.cs
deleted file mode 100644
index 394f025..0000000
--- a/Beanfun/Windows/GamePassBrowser.xaml.cs
+++ /dev/null
@@ -1,196 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Windows;
-using Microsoft.Web.WebView2.Core;
-
-namespace Beanfun
-{
- public partial class GamePassBrowser : Window
- {
- private readonly string _skey;
- private bool _hasClickedGamePass = false;
- private bool _loginCompleted = false;
-
- public GamePassBrowser(string skey)
- {
- InitializeComponent();
- _skey = skey;
- Environment.SetEnvironmentVariable(
- "WEBVIEW2_USER_DATA_FOLDER",
- Path.GetTempPath() + "\\Beanfun\\WebView2\\"
- );
- Loaded += OnLoaded;
- Closed += OnClosed;
- }
-
- private void OnClosed(object sender, EventArgs e)
- {
- if (!_loginCompleted)
- {
- // User closed window without completing login, reset bfClient
- App.MainWnd.bfClient = null;
- }
- }
-
- private async void OnLoaded(object sender, RoutedEventArgs e)
- {
- Loaded -= OnLoaded;
- this.Opacity = 0;
-
- wb_Main.CoreWebView2InitializationCompleted += OnWebViewReady;
-
- if (bool.Parse(ConfigAppSettings.GetValue("disableHardwareAcceleration", "false")))
- {
- string userDataFolder = Path.Combine(Path.GetTempPath(), "Beanfun", "WebView2");
- var options = new CoreWebView2EnvironmentOptions();
- options.AdditionalBrowserArguments = "--disable-gpu --disable-gpu-compositing";
- var env = await CoreWebView2Environment.CreateAsync(null, userDataFolder, options);
- await wb_Main.EnsureCoreWebView2Async(env);
- }
-
- wb_Main.Source = new Uri($"https://login.beanfun.com/Login/Index?pSKey={_skey}");
- }
-
- private void OnWebViewReady(object sender, CoreWebView2InitializationCompletedEventArgs e)
- {
- wb_Main.CoreWebView2.NewWindowRequested += (s, args) =>
- {
- wb_Main.CoreWebView2.Navigate(args.Uri);
- args.Handled = true;
- };
-
- wb_Main.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
-
- if (App.MainWnd.bfClient != null)
- {
- foreach (Cookie cookie in App.MainWnd.bfClient.GetCookies())
- wb_Main.CoreWebView2.CookieManager.AddOrUpdateCookie(
- wb_Main.CoreWebView2.CookieManager.CreateCookie(
- cookie.Name,
- cookie.Value,
- cookie.Domain,
- cookie.Path
- )
- );
- }
- }
-
- private async void OnNavigationCompleted(
- object sender,
- CoreWebView2NavigationCompletedEventArgs e
- )
- {
- string url = wb_Main.Source?.ToString() ?? "";
-
- // Auto-click GamePass button on login page
- if (!_hasClickedGamePass && url.Contains("Login/Index"))
- {
- _hasClickedGamePass = true;
- await wb_Main.CoreWebView2.ExecuteScriptAsync(
- @"(function() {
- var btn = document.querySelector('a.use-gama-pass');
- if (btn) btn.click();
- })()"
- );
- return;
- }
-
- // Show window once past login page
- if (_hasClickedGamePass && !url.Contains("Login/Index"))
- this.Opacity = 1;
-
- // After callback completes, beanfun redirects to SendLogin then return.aspx
- // Check if we've landed back on beanfun with bfWebToken cookie set
- if (
- url.Contains("beanfun.com")
- && (
- url.Contains("return.aspx")
- || url.Contains("index.aspx")
- || url.Contains("SendLogin")
- )
- )
- {
- await TryCompleteLogin();
- }
- }
-
- private async System.Threading.Tasks.Task TryCompleteLogin()
- {
- try
- {
- var twCookies = await wb_Main.CoreWebView2.CookieManager.GetCookiesAsync(
- "https://tw.beanfun.com"
- );
- var loginCookies = await wb_Main.CoreWebView2.CookieManager.GetCookiesAsync(
- "https://login.beanfun.com"
- );
- var newLoginCookies = await wb_Main.CoreWebView2.CookieManager.GetCookiesAsync(
- "https://tw.newlogin.beanfun.com"
- );
-
- string webToken = null;
- foreach (var cookie in twCookies)
- {
- if (cookie.Name == "bfWebToken")
- {
- webToken = cookie.Value;
- break;
- }
- }
-
- if (string.IsNullOrEmpty(webToken))
- return;
-
- // Convert WebView2 cookies to System.Net.Cookie for BeanfunClient
- var allCookies = new System.Collections.Generic.List();
- ConvertCookies(twCookies, allCookies);
- ConvertCookies(loginCookies, allCookies);
- ConvertCookies(newLoginCookies, allCookies);
-
- Dispatcher.Invoke(() =>
- {
- _loginCompleted = true;
- this.Close();
- App.MainWnd.GamePassLoginCompleted(webToken, allCookies);
- });
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"[GamePassBrowser] TryCompleteLogin failed: {ex.Message}");
- }
- }
-
- private void ConvertCookies(
- System.Collections.Generic.IReadOnlyList source,
- System.Collections.Generic.List target
- )
- {
- foreach (var wv2Cookie in source)
- {
- try
- {
- target.Add(
- new Cookie(
- wv2Cookie.Name,
- wv2Cookie.Value,
- string.IsNullOrEmpty(wv2Cookie.Path) ? "/" : wv2Cookie.Path,
- wv2Cookie.Domain.TrimStart('.')
- )
- );
- }
- catch { }
- }
- }
-
- private void wb_Main_NavigationStarting(
- object sender,
- CoreWebView2NavigationStartingEventArgs e
- )
- {
- this.Title =
- Application.Current.TryFindResource("GamePassLogin") as string ?? "GamePass Login";
- }
- }
-}
diff --git a/Beanfun/Windows/KartTools.xaml b/Beanfun/Windows/KartTools.xaml
deleted file mode 100644
index 84bc1a7..0000000
--- a/Beanfun/Windows/KartTools.xaml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/KartTools.xaml.cs b/Beanfun/Windows/KartTools.xaml.cs
deleted file mode 100644
index c28fdae..0000000
--- a/Beanfun/Windows/KartTools.xaml.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Windows;
-
-namespace Beanfun
-{
- ///
- /// KartTools.xaml 的交互逻辑
- ///
- public partial class KartTools : Window
- {
- public KartTools()
- {
- InitializeComponent();
- }
-
- private void Window_MouseLeftButtonDown(
- object sender,
- System.Windows.Input.MouseButtonEventArgs e
- )
- {
- this.DragMove();
- }
-
- private void btn_KartManageData_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/KartRider/guild/maneger_data.aspx").Show();
- }
-
- private void btn_KartRank_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/kartrider/guild/rank.aspx").Show();
- }
-
- private void btn_KartCreate_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/KartRider/guild/create.aspx").Show();
- }
-
- private void btn_KartRank_TeamIn_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/KartRider/guild/rank_team_in.aspx").Show();
- }
-
- private void btn_KartSearchMember_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/KartRider/guild/search_member.aspx").Show();
- }
-
- private void btn_KartLeaveGuildMember_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser("https://tw.beanfun.com/KartRider/guild/leave_guild_Member.aspx").Show();
- }
- }
-}
diff --git a/Beanfun/Windows/LoginRegionSelection.xaml b/Beanfun/Windows/LoginRegionSelection.xaml
deleted file mode 100644
index 258f2bc..0000000
--- a/Beanfun/Windows/LoginRegionSelection.xaml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/LoginRegionSelection.xaml.cs b/Beanfun/Windows/LoginRegionSelection.xaml.cs
deleted file mode 100644
index 1ff4390..0000000
--- a/Beanfun/Windows/LoginRegionSelection.xaml.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// LoginRegionSelection.xaml 的交互逻辑
- ///
- public partial class LoginRegionSelection : Window
- {
- public LoginRegionSelection()
- {
- InitializeComponent();
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void ButtonTW_Click(object sender, RoutedEventArgs e)
- {
- ConfigAppSettings.SetValue("loginRegion", "TW");
- this.Hide();
- App.MainWnd.Initialize();
- }
-
- private void ButtonHK_Click(object sender, RoutedEventArgs e)
- {
- ConfigAppSettings.SetValue("loginRegion", "HK");
- this.Hide();
- App.MainWnd.Initialize();
- }
-
- private void Window_Closed(object sender, System.EventArgs e)
- {
- App.Current.Shutdown();
- }
- }
-}
diff --git a/Beanfun/Windows/MapleTools.xaml b/Beanfun/Windows/MapleTools.xaml
deleted file mode 100644
index 7a44f57..0000000
--- a/Beanfun/Windows/MapleTools.xaml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/MapleTools.xaml.cs b/Beanfun/Windows/MapleTools.xaml.cs
deleted file mode 100644
index 0767464..0000000
--- a/Beanfun/Windows/MapleTools.xaml.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-using System.IO;
-using System.Windows;
-
-namespace Beanfun
-{
- ///
- /// MapleTools.xaml 的交互逻辑
- ///
- public partial class MapleTools : Window
- {
- public MapleTools()
- {
- InitializeComponent();
- }
-
- private void Window_MouseLeftButtonDown(
- object sender,
- System.Windows.Input.MouseButtonEventArgs e
- )
- {
- this.DragMove();
- }
-
- private void btn_PlayerReport_Click(object sender, RoutedEventArgs e)
- {
- if (App.LoginRegion == "HK")
- MessageBox.Show(TryFindResource("MsgPlayerReport") as string);
- //new WebBrowser("https://event.beanfun.com/customerservice/PluginReporting/PluginBoard/PluginBoardJQ.aspx").Show();
- new WebBrowser(
- "https://event.beanfun.com/customerservice/PluginReporting/PlayerReport.aspx"
- ).Show();
- }
-
- private void btn_VideoReport_Click(object sender, RoutedEventArgs e)
- {
- new WebBrowser(
- "https://event.beanfun.com/MapleStory/eventad/EventAD.aspx?EventADID=3453"
- ).Show();
- }
-
- private void btn_EquipCalculator_Click(object sender, RoutedEventArgs e)
- {
- new EquipCalculator().Show();
- }
-
- private void btn_CoreCaculator_Click(object sender, RoutedEventArgs e)
- {
- new CoreCalculator().Show();
- }
-
- private void btn_Recycling_Click(object sender, RoutedEventArgs e)
- {
- MessageBoxResult result = MessageBox.Show(
- TryFindResource("MsgRecycling") as string,
- "",
- MessageBoxButton.YesNo
- );
-
- if (result != MessageBoxResult.Yes)
- return;
-
- DirectoryInfo gameDir = new DirectoryInfo(
- Path.GetDirectoryName(App.MainWnd.settingPage.t_GamePath.Text)
- );
-
- string[] dirList = new string[]
- {
- "blob_storage",
- "GPUCache",
- "VideoDecodeStats",
- "XignCode",
- };
-
- foreach (string dir in dirList)
- {
- if (!Directory.Exists($"{gameDir.FullName}\\{dir}"))
- continue;
- try
- {
- Directory.Delete($"{gameDir.FullName}\\{dir}", true);
- }
- catch { }
- }
-
- // 清理更新失敗的緩存
- foreach (DirectoryInfo di in gameDir.GetDirectories())
- {
- try
- {
- if (di.Name.EndsWith(".$$$"))
- di.Delete(true);
- }
- catch { }
- }
-
- // 清理報錯的檔案和多餘dll
- foreach (FileInfo fi in gameDir.GetFiles())
- {
- try
- {
- if (
- fi.Name.ToLower().EndsWith(".dmp")
- || fi.Name.ToLower().Equals("localeemulator.dll")
- || fi.Name.ToLower().Equals("loaderdll.dll")
- )
- fi.Delete();
- }
- catch { }
- }
-
- MessageBox.Show(TryFindResource("MsgRecyclingDone") as string);
- }
- }
-}
diff --git a/Beanfun/Windows/ServiceAccountInfo.xaml b/Beanfun/Windows/ServiceAccountInfo.xaml
deleted file mode 100644
index 14f141d..0000000
--- a/Beanfun/Windows/ServiceAccountInfo.xaml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/ServiceAccountInfo.xaml.cs b/Beanfun/Windows/ServiceAccountInfo.xaml.cs
deleted file mode 100644
index ef66a49..0000000
--- a/Beanfun/Windows/ServiceAccountInfo.xaml.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Windows;
-using System.Windows.Input;
-using System.Windows.Media;
-
-namespace Beanfun
-{
- ///
- /// ServiceAccountInfo.xaml 的交互逻辑
- ///
- public partial class ServiceAccountInfo : Window
- {
- public ServiceAccountInfo(BeanfunClient.ServiceAccount account)
- {
- InitializeComponent();
- t_sn.Text = account.ssn;
- t_sname.Text = account.sname;
- t_id.Text = account.sid;
- t_status.Content = account.isEnable
- ? TryFindResource("Normal")
- : TryFindResource("Banned");
- t_status.Foreground = new SolidColorBrush(
- (Color)ColorConverter.ConvertFromString(account.isEnable ? "Green" : "Red")
- );
- if (account.sauthtype == null)
- {
- p_sauthtype.Visibility = Visibility.Collapsed;
- }
- else
- {
- t_sauthtype.Text = account.sauthtype;
- }
- if (account.screatetime == null)
- {
- p_screatetime.Visibility = Visibility.Collapsed;
- }
- else
- {
- t_screatetime.Content = string.Format(
- TryFindResource("CreateDate") as string,
- account.screatetime
- );
- t_screatedays.Content = getDays(account.screatetime);
- }
- if (account.slastusedtime == null)
- {
- p_slastusedtime.Visibility = Visibility.Collapsed;
- }
- else
- {
- t_slastusedtime.Content = string.Format(
- TryFindResource("LastLoginDate") as string,
- account.slastusedtime
- );
- }
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private string getDays(string time)
- {
- DateTime start = Convert.ToDateTime(time);
- DateTime end = Convert.ToDateTime(DateTime.Now);
- TimeSpan sp = end.Subtract(start);
- return Convert.ToString(sp.Days);
- }
- }
-}
diff --git a/Beanfun/Windows/UnconnectedGame_AddAccount.xaml b/Beanfun/Windows/UnconnectedGame_AddAccount.xaml
deleted file mode 100644
index ff4eae8..0000000
--- a/Beanfun/Windows/UnconnectedGame_AddAccount.xaml
+++ /dev/null
@@ -1,135 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- :
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- :
-
-
-
-
-
-
- :
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/UnconnectedGame_AddAccount.xaml.cs b/Beanfun/Windows/UnconnectedGame_AddAccount.xaml.cs
deleted file mode 100644
index b1b5d2d..0000000
--- a/Beanfun/Windows/UnconnectedGame_AddAccount.xaml.cs
+++ /dev/null
@@ -1,235 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// UnconnectedGame_AddAccount.xaml 的交互逻辑
- ///
- public partial class UnconnectedGame_AddAccount : Window
- {
- private System.Collections.Specialized.NameValueCollection payload = null;
-
- public UnconnectedGame_AddAccount()
- {
- payload = App.MainWnd.UnconnectedGame_AddAccountInit();
- if (payload == null)
- {
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- this.Close();
- return;
- }
-
- InitializeComponent();
-
- string gameName = payload.Get("GameName");
- string accountLen = payload.Get("AccountLen");
- payload.Remove("GameName");
- payload.Remove("AccountLen");
- if (payload.Get("CheckNickName") == "")
- {
- DNtr.Visibility = Visibility.Collapsed;
- lbtnCheckNickName.Visibility = Visibility.Collapsed;
- }
- payload.Remove("CheckNickName");
-
- lblAccountLen.Text = accountLen;
- lblGameName.Text = gameName;
- lblGameName1.Text = gameName;
- lblGameName2.Text = gameName;
- lblGameName3.Text = gameName;
- lblGameName4.Text = gameName;
- lblGameName5.Text = gameName;
- lbtnGameName.Text = gameName;
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Hyperlink_Click(object sender, RoutedEventArgs e)
- {
- payload = App.MainWnd.UnconnectedGame_AddUnconnectedCheck(
- txtServiceAccountID.Text,
- DNtr.Visibility == Visibility.Visible ? "" : null,
- payload
- );
- if (payload == null || payload.Get("lblErrorMessage") == "")
- {
- payload = null;
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- lblErrorMessage.Visibility = Visibility.Visible;
- lblErrorMessage.Content = payload.Get("lblErrorMessage");
- payload.Remove("lblErrorMessage");
- }
-
- private void Hyperlink_Click_1(object sender, RoutedEventArgs e)
- {
- if (lbtnCheckNickName.Visibility != Visibility.Visible)
- return;
- payload = App.MainWnd.UnconnectedGame_AddAccountCheckNickName(
- txtServiceAccountDN.Text,
- payload
- );
- if (payload == null || payload.Get("lblErrorMessage") == "")
- {
- payload = null;
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- lblErrorMessage.Visibility = Visibility.Visible;
- lblErrorMessage.Content = payload.Get("lblErrorMessage");
- payload.Remove("lblErrorMessage");
- }
-
- private void Hyperlink_Click_2(object sender, RoutedEventArgs e)
- {
- string contract = App.MainWnd.GetServiceContract();
- if (contract == "")
- {
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- }
- else
- {
- new Contract(contract).ShowDialog();
- }
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- string sAccountLen = lblAccountLen.Text;
- if (sAccountLen == null || sAccountLen == "" || !sAccountLen.Contains(" - "))
- {
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- string[] aAccountLen = sAccountLen.Split(
- new string[] { " - " },
- System.StringSplitOptions.None
- );
- byte accountLenMin = byte.Parse(aAccountLen[0]);
- byte accountLenMax = byte.Parse(aAccountLen[1]);
- if (txtServiceAccountID.Text == null || txtServiceAccountID.Text == "")
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_18") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (
- txtServiceAccountID.Text.Length < accountLenMin
- || txtServiceAccountID.Text.Length > accountLenMax
- )
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_19") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (txtNewPwd.Password == null || txtNewPwd.Password == "")
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_20") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (
- txtNewPwd.Password.Length < accountLenMin
- || txtNewPwd.Password.Length > accountLenMax
- )
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_21") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (txtNewPwd2.Password == null || txtNewPwd2.Password == "")
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_22") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (
- txtNewPwd2.Password.Length < accountLenMin
- || txtNewPwd2.Password.Length > accountLenMax
- )
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_23") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (DNtr.Visibility == Visibility.Visible)
- {
- if (txtServiceAccountDN.Text == null || txtServiceAccountDN.Text == "")
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_24") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- if (txtServiceAccountDN.Text.Length < 2 || txtServiceAccountDN.Text.Length > 6)
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_25") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- }
- if (!(bool)chkBox1.IsChecked)
- {
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_26") as string,
- TryFindResource("SystemInfo") as string
- );
- return;
- }
- string result = App.MainWnd.UnconnectedGame_AddAccount(
- txtServiceAccountID.Text,
- txtNewPwd.Password,
- txtNewPwd2.Password,
- DNtr.Visibility == Visibility.Visible ? txtServiceAccountDN.Text : null,
- payload
- );
- if (result == "")
- this.Close();
- else if (result == null)
- MessageBox.Show(
- TryFindResource("UnconnectedGame_AddAccount_27") as string,
- TryFindResource("SystemInfo") as string
- );
- else
- {
- lblErrorMessage.Visibility = Visibility.Visible;
- lblErrorMessage.Content = result;
- }
- }
- }
-}
diff --git a/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml b/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml
deleted file mode 100644
index d8461d2..0000000
--- a/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml.cs b/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml.cs
deleted file mode 100644
index 224fb02..0000000
--- a/Beanfun/Windows/UnconnectedGame_ChangePassword.xaml.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Text.RegularExpressions;
-using System.Windows;
-using System.Windows.Input;
-
-namespace Beanfun
-{
- ///
- /// UnconnectedGame_ChangePassword.xaml 的交互逻辑
- ///
- public partial class UnconnectedGame_ChangePassword : Window
- {
- public UnconnectedGame_ChangePassword()
- {
- InitializeComponent();
- }
-
- private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- this.DragMove();
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- string result = App.MainWnd.UnconnectedGame_ChangePassword(txtEmail.Text);
- if (result == null)
- MessageBox.Show(
- TryFindResource("UnknownError") as string,
- TryFindResource("SystemInfo") as string
- );
- else if (result.StartsWith("verify_code"))
- {
- MessageBox.Show(
- string.Format(
- Regex.Unescape(TryFindResource("MsgChangePassword") as string),
- result.Replace("verify_code", "")
- ),
- TryFindResource("DataSended") as string
- );
- this.Close();
- }
- else
- {
- lblErrorMessage.Visibility = Visibility.Visible;
- lblErrorMessage.Content = result;
- }
- }
- }
-}
diff --git a/Beanfun/Windows/WebBrowser.xaml b/Beanfun/Windows/WebBrowser.xaml
deleted file mode 100644
index 13cda0a..0000000
--- a/Beanfun/Windows/WebBrowser.xaml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/Beanfun/Windows/WebBrowser.xaml.cs b/Beanfun/Windows/WebBrowser.xaml.cs
deleted file mode 100644
index 12d3ea9..0000000
--- a/Beanfun/Windows/WebBrowser.xaml.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Windows;
-using Microsoft.Web.WebView2.Core;
-
-namespace Beanfun
-{
- ///
- /// WebBrowser.xaml 的交互逻辑
- ///
- public partial class WebBrowser : Window
- {
- private readonly string _initialUri;
-
- public WebBrowser(string uri)
- {
- InitializeComponent();
- _initialUri = uri;
- Environment.SetEnvironmentVariable(
- "WEBVIEW2_USER_DATA_FOLDER",
- Path.GetTempPath() + "\\Beanfun\\WebView2\\"
- );
- Loaded += WebBrowser_Loaded;
- }
-
- private async void WebBrowser_Loaded(object sender, RoutedEventArgs e)
- {
- Loaded -= WebBrowser_Loaded;
- wb_Main.CoreWebView2InitializationCompleted +=
- Wb_Main_CoreWebView2InitializationCompleted;
-
- if (bool.Parse(ConfigAppSettings.GetValue("disableHardwareAcceleration", "false")))
- {
- string userDataFolder = Path.Combine(Path.GetTempPath(), "Beanfun", "WebView2");
- var options = new CoreWebView2EnvironmentOptions();
- options.AdditionalBrowserArguments = "--disable-gpu --disable-gpu-compositing";
- CoreWebView2Environment env = await CoreWebView2Environment.CreateAsync(
- null,
- userDataFolder,
- options
- );
- await wb_Main.EnsureCoreWebView2Async(env);
- }
-
- wb_Main.Source = new Uri(_initialUri);
- }
-
- private void Wb_Main_CoreWebView2InitializationCompleted(
- object sender,
- CoreWebView2InitializationCompletedEventArgs e
- )
- {
- wb_Main.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
- wb_Main.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted;
- if (App.MainWnd.bfClient != null)
- {
- foreach (Cookie cookie in App.MainWnd.bfClient.GetCookies())
- wb_Main.CoreWebView2.CookieManager.AddOrUpdateCookie(
- wb_Main.CoreWebView2.CookieManager.CreateCookie(
- cookie.Name,
- cookie.Value,
- cookie.Domain,
- cookie.Path
- )
- );
- }
- }
-
- private void CoreWebView2_NavigationCompleted(
- object sender,
- CoreWebView2NavigationCompletedEventArgs e
- )
- {
- this.Title = wb_Main.CoreWebView2.DocumentTitle;
- }
-
- private void CoreWebView2_NewWindowRequested(
- object sender,
- CoreWebView2NewWindowRequestedEventArgs e
- )
- {
- wb_Main.CoreWebView2.Navigate(e.Uri);
- e.Handled = true;
- }
-
- private void Window_MouseLeftButtonDown(
- object sender,
- System.Windows.Input.MouseButtonEventArgs e
- )
- {
- this.DragMove();
- }
-
- private void wb_Main_NavigationStarting(
- object sender,
- CoreWebView2NavigationStartingEventArgs e
- )
- {
- t_URI.Text = e.Uri;
- }
- }
-}
diff --git a/Beanfun/Lang/en.xaml b/Lang/en.xaml
similarity index 100%
rename from Beanfun/Lang/en.xaml
rename to Lang/en.xaml
diff --git a/Beanfun/Lang/zh-Hans.xaml b/Lang/zh-Hans.xaml
similarity index 99%
rename from Beanfun/Lang/zh-Hans.xaml
rename to Lang/zh-Hans.xaml
index d2e4b15..57ae3ad 100644
--- a/Beanfun/Lang/zh-Hans.xaml
+++ b/Lang/zh-Hans.xaml
@@ -229,6 +229,7 @@
未找到完美核心组合
第{0}组组合:
选择游戏
+ 工具箱
车队操作
车队管理
车队排名
diff --git a/Beanfun/Lang/zh.xaml b/Lang/zh.xaml
similarity index 100%
rename from Beanfun/Lang/zh.xaml
rename to Lang/zh.xaml
diff --git a/README.md b/README.md
index 1359053..b83894f 100644
--- a/README.md
+++ b/README.md
@@ -1,122 +1,125 @@
# Beanfun
[](https://github.com/pungin/Beanfun/releases)
-[](https://github.com/pungin/Beanfun/actions/workflows/format-check.yml)
+[](https://github.com/pungin/Beanfun/actions/workflows/ci.yml)
-> **遊戲橘子數位科技旗下遊戲的第三方啟動器**
+> **遊戲橘子旗下科技紅利遊戲的第三方啟動器**
-⚠️ **免責聲明:** 本程式 **不是** 遊戲橘子數位科技開發的官方客戶端程式。關於遊戲帳號使用第三方的方式登入,請再三斟酌,並請確認您下載當前程式的途徑是否安全。
+**免責聲明:** 本軟體 **不是** 遊戲橘子旗下科技所開發的官方客戶端程式。若您的帳號使用第三方的方式登錄,請自行三思並且確認下載當前程式的來源是否安全。
-* 本程式使用部分 `BeanfunLogin` 的代碼。
-* 程式使用 [Locale_Remulator](https://github.com/InWILL/Locale_Remulator) 作為區域模擬元件,支持 32-bit 和 64-bit 遊戲。
+- 本程式使用部份 `BeanfunLogin` 的代碼。
+- 程式使用 [Locale_Remulator](https://github.com/InWILL/Locale_Remulator) 作為語言模擬元件,支援 32-bit 及 64-bit 遊戲。
---
## 下載與使用 (Getting Started)
### 系統要求 (Prerequisites)
-* **作業系統:** Windows 10 或以上
-* **必要元件:** [Microsoft Visual C++ Redistributable](https://docs.microsoft.com/zh-CN/cpp/windows/latest-supported-vc-redist?view=msvc-170)
+
+- **作業系統:** Windows 10 或以上
+- **必備元件:** [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)(Windows 11 已預裝)
### 使用方法 (Usage)
+
1. 前往 **[最新發行版 (Releases)](https://github.com/pungin/Beanfun/releases/latest)** 下載最新的 `Beanfun.exe`。
-2. 下載後放在任意全英文路徑的資料夾,直接運行即可。
+2. 下載後放至任意全英文路徑的資料夾,直接執行即可。
-> 💡 **運作原理說明:** 啟動遊戲時,程式會在當前資料夾釋放 `LRProc.dll` 和 `LRHookx32.dll` 或 `LRHookx64.dll` 文件。
-> * `LRProc.dll` - 將 Hook dll 載入到遊戲中
-> * `LRHookx32.dll` 或 `LRHookx64.dll` - 區域模擬元件
+> **⚠ 注意事項說明:**
+>
+> - 啟動遊戲時程式會在執行資料夾生成 `LRProc.exe`、`LRHookx32.dll`、`LRHookx64.dll` 等件。
+> - `LRProc.exe` — 負責 Hook DLL 載入至程序中
+> - `LRHookx32.dll` / `LRHookx64.dll` — 語言模擬元件
---
## 技術棧 (Built With)
-* **[.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)** - 目標框架(Self-contained,使用者不需另外安裝 Runtime)
-* **[ini-parser-netstandard](https://github.com/lukazh/ini-parser-standard)** - ini 設定檔元件
-* **[log4net](https://logging.apache.org/log4net/)** - 日誌記錄元件
-* **[Newtonsoft.Json](https://www.newtonsoft.com/json)** - JSON 解析元件
-* **[Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2)** - 內嵌瀏覽器元件
-* **[Detours](https://github.com/microsoft/Detours)** - 用於 Hook ANSI/Unicode 函數
-* **[Locale_Remulator](https://github.com/InWILL/Locale_Remulator)** - 區域模擬元件
+- **[Tauri v2](https://tauri.app/)** — 桌面殼層(WebView2)
+- **[Rust](https://www.rust-lang.org/)** — 後端核心
+- **[Vue 3](https://vuejs.org/) + [TypeScript](https://www.typescriptlang.org/) + [Vite](https://vite.dev/)** — 前端框架
+- **[Element Plus](https://element-plus.org/)** — UI 元件庫
+- **[Locale_Remulator](https://github.com/InWILL/Locale_Remulator)** — 語言模擬元件
---
-## 維護與貢獻規範 (Maintenance & Contribution)
-
-為了確保專案品質以及自動化版本控制的穩定性,所有貢獻者請遵循以下標準開發流程:
-
-### 流程:Fork ➔ Test ➔ PR ➔ Format ➔ Approve
+## 架構 (Architecture)
-1. **Fork 專案**:將本倉庫 Fork 到你個人的 GitHub 帳號下進行開發。
-2. **本地測試 (Local Test)**:
- * 在完成功能開發或 Bug 修復後,務必在本地環境進行編譯與功能測試。
- * 確保程式能正常啟動,且不會影響現有的登入功能。
-3. **提交 Pull Request (PR)**:
- * 將你的改動 Push 到你 Fork 的倉庫,並向本專案發起 PR。在 PR 描述中清晰說明你修改的內容與原因。
-4. **執行程式碼格式化 (CSharpier) [強制]**:
- * 提交前請務必執行 CSharpier 確保代碼風格一致,否則 CI 檢查可能會失敗。
- ```bash
- dotnet tool restore
- dotnet csharpier format .
- ```
-5. **審核與合併 (Approve & Merge)**:
- * 維護者審核通過並合併 PR 後,系統會自動接手後續的版號遞增與發布。
+```
+Beanfun/
+├── src/ # Vue 3 前端
+│ ├── pages/ # 頁面元件 (Login, GameList, Settings…)
+│ ├── composables/ # 組合式函式 (useAuth, useGameLauncher…)
+│ ├── stores/ # Pinia 狀態管理
+│ ├── services/ # Tauri IPC 呼叫封裝
+│ ├── i18n / locales/ # vue-i18n 多語系
+│ ├── styles/ # 全域樣式
+│ └── windows/ # 多視窗進入點 (In-App Browser)
+├── src-tauri/ # Rust 後端 (Tauri v2)
+│ ├── src/
+│ │ ├── commands/ # IPC command handlers
+│ │ ├── core/ # 核心邏輯 (parser, crypto, version)
+│ │ └── services/ # 服務層
+│ │ ├── beanfun/ # 登入 / 帳號 / session
+│ │ ├── config/ # XML 設定檔讀寫
+│ │ ├── game/ # 遊戲啟動 + LocaleRemulator
+│ │ ├── process/ # 視窗操作 / PostMessage
+│ │ ├── storage/ # Users.dat 加解密 (DPAPI)
+│ │ ├── registry/ # Windows Registry
+│ │ ├── updater/ # 自動更新檢查
+│ │ └── system/ # 系統工具 (open URL…)
+│ ├── LocaleRemulator/ # LR 預編譯二進位檔
+│ └── tests/ # 整合測試
+├── tests/ # 前端單元測試 (Vitest)
+├── scripts/ # 建置 / 轉換腳本
+├── Lang/ # WPF 時代 XAML 語系檔 → i18n 來源
+└── .github/workflows/ # CI (lint, format, test, build)
+```
---
-## 開發與發佈 (Development & Release)
-
-### 全自動化發佈流程 (CI/CD)
-
-> 💡 **核心原則**:本專案的版本發行 **完全零人工作業**。開發者**不需要**在本地自行編譯、打包或手動建立 Release。所有的版本號運算、資訊寫入 (InformationalVersion)、執行檔建置與雙語 Release Notes 的生成,均由 GitHub Actions 全自動處理。
+## 開發 (Development)
-若要發佈新版本,請直接至 GitHub 的 [Actions 頁面](../../actions/workflows/build-and-release.yml) 手動觸發 **Build and Release** workflow 即可。
+### 環境需求
-#### 發佈參數說明
+| 需求 | 版本 |
+| ------------------------- | ------------------------------------- |
+| Node.js | >= 22 LTS |
+| Rust | stable (x86_64-pc-windows-msvc) |
+| WebView2 Runtime | Windows 11 預裝;Windows 10 需安裝 |
+| Visual Studio Build Tools | 安裝 **Desktop development with C++** |
-| 參數 | 說明 | 預設值 |
-|------|------|--------|
-| `release_type` | `release`(正式版)或 `prerelease`(測試版) | `prerelease` |
-| `version_increment` | 版本遞增方式:`patch` / `minor` / `major` | `patch` |
-| `release_name` | 自訂發佈名稱(留空將由系統自動產生) | 空 |
+### 快速開始
-#### 自動化版本控制機制
-
-系統會將完整的版號(包含 Patch 與精準的 UTC Timestamp)**強制注入**至執行檔中,確保程式內顯示的版號與 GitHub 完全一致。Tag 格式為 `v{major}.{minor}.{patch}.{timestamp}`(例如 `v5.8.13.2603311234`)。
-
-| 操作 | 觸發情境範例 | AssemblyInfo.cs 變化 |
-|------|------|-----------------|
-| patch | v5.8.12 → v5.8.13 | 自動更新為 5.8.13.* |
-| minor | v5.8.13 → v5.9.0 | 自動更新為 5.9.0.* |
-| major | v5.9.0 → v6.0.0 | 自動更新為 6.0.0.* |
-
-* **智慧遞增**:Patch 值會自動從 Git 最新 Tag 解析並 +1。
-* **Release Notes 生成**:自動抓取版本間的 Commits,排版成包含中英雙語、支援摺疊技術細節的 Release 頁面。
-* **自動 Commit**:產生新 Tag 後,系統會自動將修改後的 `AssemblyInfo.cs` Commit 並 Push 回儲存庫。
+```sh
+npm install
+npm run tauri dev
+```
-### 程式碼格式化
+### 常用指令
-本專案使用 [CSharpier](https://csharpier.com/) 作為程式碼格式化工具。
+```sh
+# 前端
+npm run lint # ESLint
+npm run format:check # Prettier
+npm run typecheck # TypeScript 型別檢查
+npm run test # Vitest 單元測試
-```bash
-# 安裝還原工具
-dotnet tool restore
+# 後端 (src-tauri/)
+cargo fmt --check # Rust 格式檢查
+cargo clippy -- -D warnings
+cargo test
+```
-# 格式化所有 .cs 檔案
-dotnet csharpier format .
+---
-# 檢查格式(不修改檔案)
-dotnet csharpier check .
-```
+## 貢獻 (Contributing)
-### 本地測試打包 (僅供開發除錯用)
+1. 從 `code` 分支出新 feature branch。
+2. PR 到 `code` 時會跑 CI(lint / format / typecheck / test)。
+3. 送 PR 前請先跑 `npm run format` + `cargo fmt`。
-若你需要在本地端測試打包流程,可使用以下指令。
-*(⚠️ 注意:此產出的 `Beanfun.exe` 僅供本地除錯,切勿手動上傳至 GitHub Release)*
+---
-```bash
-# 清理專案
-dotnet clean Beanfun/Beanfun.csproj -c Release
+## 授權 (License)
-# 建置單一 exe(Self-contained)
-dotnet publish Beanfun/Beanfun.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:IncludeAllContentForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=none -o publish
-```
\ No newline at end of file
+與主專案 [pungin/Beanfun](https://github.com/pungin/Beanfun) 相同。
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..2a4f2e9
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,31 @@
+import pluginVue from 'eslint-plugin-vue'
+import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
+import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
+
+export default defineConfigWithVueTs(
+ {
+ name: 'app/files-to-lint',
+ files: ['**/*.{ts,mts,tsx,vue}'],
+ },
+ {
+ name: 'app/files-to-ignore',
+ ignores: [
+ 'dist/**',
+ 'src-tauri/target/**',
+ 'src-tauri/gen/**',
+ 'mockups/**',
+ 'node_modules/**',
+ // Vue + Vite scaffold type shim uses `{}` / `any` by design.
+ '**/vite-env.d.ts',
+ // Generated by `cargo run --example export_bindings` (tauri-specta).
+ // The file ships an `// @ts-nocheck` header for vue-tsc and otherwise
+ // contains framework prelude (`TAURI_CHANNEL`, `__makeEvents__`,
+ // `Result` with `any` fallbacks) that we don't own and cannot
+ // edit without it being clobbered on the next regeneration.
+ 'src/types/bindings.ts',
+ ],
+ },
+ pluginVue.configs['flat/essential'],
+ vueTsConfigs.recommended,
+ skipFormatting,
+)
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..99f203f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Tauri + Vue + Typescript App
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0252ba8
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5722 @@
+{
+ "name": "beanfun",
+ "version": "6.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "beanfun",
+ "version": "6.0.0",
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.2",
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-dialog": "2.7.0",
+ "@tauri-apps/plugin-opener": "^2",
+ "element-plus": "^2.13.7",
+ "pinia": "^3.0.4",
+ "pinia-plugin-persistedstate": "^4.7.1",
+ "vue": "^3.5.13",
+ "vue-i18n": "^11.3.2",
+ "vue-router": "^4.6.4",
+ "vuedraggable": "^4.1.0"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2",
+ "@types/node": "^25.6.0",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/eslint-config-prettier": "^10.2.0",
+ "@vue/eslint-config-typescript": "^14.7.0",
+ "@vue/test-utils": "^2.4.6",
+ "eslint": "^9.39.4",
+ "eslint-plugin-vue": "^10.8.0",
+ "fast-xml-parser": "^5.7.1",
+ "jsdom": "^29.0.2",
+ "prettier": "^3.8.3",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.3",
+ "vitest": "^4.1.4",
+ "vue-tsc": "^2.1.10"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.10.tgz",
+ "integrity": "sha512-KyOb19eytNSELkmdqzZZUXWCU25byIlOld5qVFg0RYdS0T3tt7jeDByxk9hIAC73frclD8GKrHttr0SUjKCCdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz",
+ "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz",
+ "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz",
+ "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+ "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@intlify/core-base": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.2.tgz",
+ "integrity": "sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/devtools-types": "11.3.2",
+ "@intlify/message-compiler": "11.3.2",
+ "@intlify/shared": "11.3.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/devtools-types": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.2.tgz",
+ "integrity": "sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/core-base": "11.3.2",
+ "@intlify/shared": "11.3.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/message-compiler": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.2.tgz",
+ "integrity": "sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/shared": "11.3.2",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/shared": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.2.tgz",
+ "integrity": "sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@nodable/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tauri-apps/api": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
+ "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
+ "license": "Apache-2.0 OR MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ }
+ },
+ "node_modules/@tauri-apps/cli": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
+ "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "bin": {
+ "tauri": "tauri.js"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/tauri"
+ },
+ "optionalDependencies": {
+ "@tauri-apps/cli-darwin-arm64": "2.10.1",
+ "@tauri-apps/cli-darwin-x64": "2.10.1",
+ "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
+ "@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
+ "@tauri-apps/cli-linux-arm64-musl": "2.10.1",
+ "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
+ "@tauri-apps/cli-linux-x64-gnu": "2.10.1",
+ "@tauri-apps/cli-linux-x64-musl": "2.10.1",
+ "@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
+ "@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
+ "@tauri-apps/cli-win32-x64-msvc": "2.10.1"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-arm64": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
+ "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-darwin-x64": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
+ "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
+ "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
+ "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-arm64-musl": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
+ "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
+ "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-gnu": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
+ "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-linux-x64-musl": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
+ "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
+ "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
+ "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/cli-win32-x64-msvc": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
+ "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 OR MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tauri-apps/plugin-dialog": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz",
+ "integrity": "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.10.1"
+ }
+ },
+ "node_modules/@tauri-apps/plugin-opener": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
+ "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.8.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
+ "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/type-utils": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.58.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
+ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
+ "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.58.2",
+ "@typescript-eslint/types": "^8.58.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
+ "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
+ "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
+ "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
+ "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
+ "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.58.2",
+ "@typescript-eslint/tsconfig-utils": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
+ "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
+ "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz",
+ "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.4",
+ "@vitest/utils": "4.1.4",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz",
+ "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+ "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz",
+ "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.4",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz",
+ "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.4",
+ "@vitest/utils": "4.1.4",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
+ "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+ "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.4",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
+ "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/shared": "3.5.32",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
+ "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
+ "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/compiler-core": "3.5.32",
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
+ "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+ "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.7.9"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+ "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.7.9",
+ "birpc": "^2.3.0",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.7.9",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+ "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/eslint-config-prettier": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz",
+ "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-config-prettier": "^10.0.1",
+ "eslint-plugin-prettier": "^5.2.2"
+ },
+ "peerDependencies": {
+ "eslint": ">= 8.21.0",
+ "prettier": ">= 3.0.0"
+ }
+ },
+ "node_modules/@vue/eslint-config-typescript": {
+ "version": "14.7.0",
+ "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.7.0.tgz",
+ "integrity": "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^8.56.0",
+ "fast-glob": "^3.3.3",
+ "typescript-eslint": "^8.56.0",
+ "vue-eslint-parser": "^10.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.10.0 || ^10.0.0",
+ "eslint-plugin-vue": "^9.28.0 || ^10.0.0",
+ "typescript": ">=4.8.4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
+ "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
+ "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
+ "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.32",
+ "@vue/runtime-core": "3.5.32",
+ "@vue/shared": "3.5.32",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
+ "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32"
+ },
+ "peerDependencies": {
+ "vue": "3.5.32"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
+ "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/test-utils": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
+ "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-beautify": "^1.14.9",
+ "vue-component-type-helpers": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/test-utils/node_modules/vue-component-type-helpers": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
+ "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+ "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "12.0.0",
+ "@vueuse/shared": "12.0.0",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+ "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+ "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+ "license": "MIT",
+ "dependencies": {
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-anything": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+ "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
+ "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "^9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.13.7",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz",
+ "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.2.0",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.17.20",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "12.0.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.19",
+ "lodash": "^4.17.23",
+ "lodash-es": "^4.17.23",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0",
+ "vue-component-type-helpers": "^3.2.4"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
+ "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.1",
+ "synckit": "^0.11.12"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-vue": {
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz",
+ "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "natural-compare": "^1.4.0",
+ "nth-check": "^2.1.1",
+ "postcss-selector-parser": "^7.1.0",
+ "semver": "^7.6.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+ "@typescript-eslint/parser": "^7.0.0 || ^8.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "vue-eslint-parser": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@stylistic/eslint-plugin": {
+ "optional": true
+ },
+ "@typescript-eslint/parser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-vue/node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-xml-builder": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
+ "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.1.3"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz",
+ "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.1.5",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.2.3"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-what": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+ "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "29.0.2",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz",
+ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.5",
+ "@asamuzakjp/dom-selector": "^7.0.6",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.1",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.7",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.24.5",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "11.3.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
+ "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+ "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/devtools-api": "^7.7.7"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.5.0",
+ "vue": "^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pinia-plugin-persistedstate": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
+ "integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": ">=3.0.0",
+ "@pinia/nuxt": ">=0.10.0",
+ "pinia": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ },
+ "@pinia/nuxt": {
+ "optional": true
+ },
+ "pinia": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
+ "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sortablejs": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+ "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
+ "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/superjson": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+ "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/synckit": {
+ "version": "0.11.12",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
+ "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.9"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
+ "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.0.28",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
+ "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.28"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.28",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz",
+ "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz",
+ "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
+ "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz",
+ "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.4",
+ "@vitest/mocker": "4.1.4",
+ "@vitest/pretty-format": "4.1.4",
+ "@vitest/runner": "4.1.4",
+ "@vitest/snapshot": "4.1.4",
+ "@vitest/spy": "4.1.4",
+ "@vitest/utils": "4.1.4",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.4",
+ "@vitest/browser-preview": "4.1.4",
+ "@vitest/browser-webdriverio": "4.1.4",
+ "@vitest/coverage-istanbul": "4.1.4",
+ "@vitest/coverage-v8": "4.1.4",
+ "@vitest/ui": "4.1.4",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
+ "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-sfc": "3.5.32",
+ "@vue/runtime-dom": "3.5.32",
+ "@vue/server-renderer": "3.5.32",
+ "@vue/shared": "3.5.32"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-component-type-helpers": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz",
+ "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==",
+ "license": "MIT"
+ },
+ "node_modules/vue-eslint-parser": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz",
+ "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "eslint-scope": "^8.2.0 || ^9.0.0",
+ "eslint-visitor-keys": "^4.2.0 || ^5.0.0",
+ "espree": "^10.3.0 || ^11.0.0",
+ "esquery": "^1.6.0",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/vue-i18n": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.2.tgz",
+ "integrity": "sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/core-base": "11.3.2",
+ "@intlify/devtools-types": "11.3.2",
+ "@intlify/shared": "11.3.2",
+ "@vue/devtools-api": "^6.5.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vue-i18n/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/vuedraggable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
+ "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+ "license": "MIT",
+ "dependencies": {
+ "sortablejs": "1.14.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.1"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7fb558d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "beanfun",
+ "private": true,
+ "version": "6.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview",
+ "tauri": "tauri",
+ "typecheck": "vue-tsc --noEmit",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.2",
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-dialog": "2.7.0",
+ "@tauri-apps/plugin-opener": "^2",
+ "element-plus": "^2.13.7",
+ "pinia": "^3.0.4",
+ "pinia-plugin-persistedstate": "^4.7.1",
+ "vue": "^3.5.13",
+ "vue-i18n": "^11.3.2",
+ "vue-router": "^4.6.4",
+ "vuedraggable": "^4.1.0"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2",
+ "@types/node": "^25.6.0",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/eslint-config-prettier": "^10.2.0",
+ "@vue/eslint-config-typescript": "^14.7.0",
+ "@vue/test-utils": "^2.4.6",
+ "eslint": "^9.39.4",
+ "eslint-plugin-vue": "^10.8.0",
+ "fast-xml-parser": "^5.7.1",
+ "jsdom": "^29.0.2",
+ "prettier": "^3.8.3",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.3",
+ "vitest": "^4.1.4",
+ "vue-tsc": "^2.1.10"
+ }
+}
diff --git a/public/tauri.svg b/public/tauri.svg
new file mode 100644
index 0000000..31b62c9
--- /dev/null
+++ b/public/tauri.svg
@@ -0,0 +1,6 @@
+
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..8c6a8b0
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,3 @@
+max_width = 100
+use_small_heuristics = "Default"
+newline_style = "Unix"
diff --git a/scripts/convert-lang.mjs b/scripts/convert-lang.mjs
new file mode 100644
index 0000000..7e4f56d
--- /dev/null
+++ b/scripts/convert-lang.mjs
@@ -0,0 +1,158 @@
+#!/usr/bin/env node
+// @ts-check
+
+/**
+ * Convert legacy WPF `Beanfun/Lang/*.xaml` resource dictionaries
+ * into vue-i18n flat KV JSON files in `src/locales/`.
+ *
+ * Run from the project root:
+ *
+ * node scripts/convert-lang.mjs
+ *
+ * # Mapping (P11 Q3 = A: WPF key 1:1)
+ *
+ * Beanfun/Lang/zh.xaml → src/locales/zh-TW.json
+ * Beanfun/Lang/zh-Hans.xaml → src/locales/zh-CN.json
+ * Beanfun/Lang/en.xaml → src/locales/en-US.json
+ *
+ * # What is extracted
+ *
+ * Only `V` entries become
+ * keys in the output JSON. The XAML files also contain non-string
+ * resources (`` for the SVG-style logo path,
+ * `` for embedded mini-rich-text views) that are not
+ * translatable strings and would not survive a JSON round-trip; the
+ * Vue port re-implements those visual resources directly in the
+ * page templates instead. See P12 page rebuild for the migration
+ * plan.
+ *
+ * # Placeholder & escape conventions (preserved verbatim)
+ *
+ * - `{0}`, `{1}`, … — kept as-is. vue-i18n's list-mode interpolation
+ * accepts them via `t(key, [arg0, arg1])`, matching WPF's
+ * `string.Format` semantics 1:1.
+ * - `%0d` — the WPF source uses URI-style escapes for newlines in a
+ * handful of strings (e.g. `FeedbackText`). Kept as raw text; the
+ * consuming page is responsible for the same `Uri.UnescapeDataString`
+ * step the WPF code does.
+ * - `<R>`, `<B>`, etc. — XML entity escapes for
+ * nested mini-markup (`…`) used by WPF's
+ * `RichTextBlock`. fast-xml-parser auto-decodes these so the
+ * JSON value contains the unescaped `` / `` form. The Vue
+ * port renders these strings via `` (P12) and parses
+ * the same syntax.
+ *
+ * # Why a separate parser export
+ *
+ * `parseXamlStrings` is exported so the vitest spec can drive it
+ * with inline fixtures without touching the real WPF files. The
+ * `main` entry point only runs when this module is executed
+ * directly (CLI use); importing it in tests does not write
+ * anything to disk.
+ */
+
+import { XMLParser } from 'fast-xml-parser'
+import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+/** Project root (one level above `scripts/`). */
+const PROJECT_ROOT = resolve(__dirname, '..')
+
+const WPF_LANG_DIR = resolve(PROJECT_ROOT, 'Lang')
+const FRONTEND_LOCALES_DIR = resolve(PROJECT_ROOT, 'src', 'locales')
+
+/**
+ * @typedef {{ source: string; target: string }} LocaleMapEntry
+ */
+
+/** @type {LocaleMapEntry[]} */
+export const LOCALE_FILE_MAP = [
+ { source: 'zh.xaml', target: 'zh-TW.json' },
+ { source: 'zh-Hans.xaml', target: 'zh-CN.json' },
+ { source: 'en.xaml', target: 'en-US.json' },
+]
+
+/**
+ * Parse a XAML resource-dictionary string and return only the
+ * `V` entries as a flat
+ * object.
+ *
+ * Order of keys follows the order they appear in the source XAML so
+ * that diffs remain reviewable when WPF strings are re-translated
+ * upstream (insertion order is preserved by `Object.keys` in
+ * modern engines).
+ *
+ * @param {string} xaml — UTF-8 XAML source.
+ * @returns {Record}
+ */
+export function parseXamlStrings(xaml) {
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ attributeNamePrefix: '@_',
+ textNodeName: '#text',
+ parseAttributeValue: false,
+ parseTagValue: false,
+ trimValues: false,
+ // Ensure repeated `` siblings always become an
+ // array even when there's only one — simpler downstream branching.
+ isArray: (tagName) => tagName === 'system:String',
+ })
+
+ const tree = parser.parse(xaml)
+ const root = tree.ResourceDictionary
+ if (!root) {
+ throw new Error('XAML root element not found')
+ }
+
+ /** @type {unknown} */
+ const stringNodes = root['system:String']
+ if (!stringNodes) return {}
+ if (!Array.isArray(stringNodes)) {
+ throw new Error('expected system:String to be an array (isArray hint failed)')
+ }
+
+ /** @type {Record} */
+ const out = {}
+ for (const node of stringNodes) {
+ if (typeof node !== 'object' || node === null) continue
+ const obj = /** @type {Record} */ (node)
+ const key = obj['@_x:Key']
+ if (typeof key !== 'string' || key.length === 0) continue
+ const text = obj['#text']
+ out[key] = typeof text === 'string' ? text : ''
+ }
+ return out
+}
+
+/**
+ * CLI entry point. Reads each XAML file in {@link LOCALE_FILE_MAP},
+ * converts it via {@link parseXamlStrings}, and writes the JSON
+ * artefact to `src/locales/`. Logs a one-line summary per file to
+ * stdout; throws (non-zero exit via the caller) on any I/O or parse
+ * error so CI pipelines can detect drift.
+ */
+export function convertAllLocales() {
+ mkdirSync(FRONTEND_LOCALES_DIR, { recursive: true })
+ for (const { source, target } of LOCALE_FILE_MAP) {
+ const inPath = resolve(WPF_LANG_DIR, source)
+ const outPath = resolve(FRONTEND_LOCALES_DIR, target)
+ const xaml = readFileSync(inPath, 'utf8')
+ const obj = parseXamlStrings(xaml)
+ writeFileSync(outPath, JSON.stringify(obj, null, 2) + '\n', 'utf8')
+ console.log(`convert-lang: ${source} → ${target} (${Object.keys(obj).length} keys)`)
+ }
+}
+
+const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(__filename)
+if (isMain) {
+ try {
+ convertAllLocales()
+ } catch (err) {
+ console.error('convert-lang: failed', err)
+ process.exit(1)
+ }
+}
diff --git a/src-tauri/.cargo/config.toml b/src-tauri/.cargo/config.toml
new file mode 100644
index 0000000..797f0c2
--- /dev/null
+++ b/src-tauri/.cargo/config.toml
@@ -0,0 +1,60 @@
+# Cargo build configuration scoped to the `beanfun` crate.
+#
+# This file solves a Windows-specific link-time problem that surfaces
+# when an *example* binary (like `examples/export_bindings.rs`) or a
+# unit-test binary statically links anything that pulls in
+# `webview2-com-sys`'s import lib for `WebView2Loader.dll` (e.g.
+# `tauri::Wry`, `tauri::Window`, `tauri::test::MockRuntime`,
+# `tauri_specta::Builder`).
+#
+# Cargo only copies `WebView2Loader.dll` next to the *main* Tauri
+# binary during `cargo tauri dev` / `cargo tauri build`; it does NOT
+# copy the DLL next to `target/debug/examples/*.exe` or
+# `target/debug/deps/*-.exe`. As a result, those binaries hit
+# `STATUS_ENTRYPOINT_NOT_FOUND` (0xc0000139) at process-start time
+# even though they never call into a webview themselves.
+#
+# Tauri tracks this as a known issue across several reports:
+# - tauri-apps/tauri#11028 (specta export failing on Windows)
+# - tauri-apps/tauri#13419 (cargo test failing on Windows)
+# - tauri-apps/tauri#13948 (workspace child crate startup fail)
+# - tauri-apps/tauri#14580 (lib tests touching tauri::Window)
+#
+# The fix below uses MSVC's delay-load mechanism: the linker emits a
+# stub instead of an unconditional import-table entry for
+# `WebView2Loader.dll`, so the loader doesn't try to resolve the DLL
+# until the first call into one of its exported functions.
+#
+# Effect on each build target:
+# - `lib` (rlib / cdylib / staticlib) — link metadata only, unchanged.
+# - `bin` (`Beanfun.exe`) — `run()` always wires the webview, so
+# the first WebView2 call happens during `tauri::Builder::run`. DLL
+# load timing shifts from process-start to a few hundred
+# microseconds later; behavioural difference is unobservable in
+# practice. If `WebView2Loader.dll` is missing entirely (which the
+# Tauri installer prevents via the WebView2 Runtime dependency),
+# the failure surfaces as a panic from `Builder::run` instead of an
+# `STATUS_ENTRYPOINT_NOT_FOUND` exit code — strictly more debuggable.
+# - `example` (`export_bindings`) — never calls into the webview, so
+# the DLL is never loaded; the binary runs cleanly even on machines
+# that don't have `WebView2Loader.dll` next to the exe.
+# - `test` — same as `example`. Existing tests deliberately avoid
+# instantiating `tauri_specta::Builder` (see the
+# `commands::bindings_file_tests` module docs for the
+# `webview2-com-sys` linkage analysis); delay-load is belt-and-
+# suspenders for any future test that does the same dance the
+# example binary does.
+#
+# `delayimp.lib` provides MSVC's runtime helper that resolves the
+# delay-load stub on first use. It must be linked alongside the
+# `/DELAYLOAD:` directive — without it the linker rejects the
+# directive with LNK1194.
+#
+# Scoped to `x86_64-pc-windows-msvc` (Tauri's only supported Windows
+# target for Beanfun) so non-Windows hosts running `cargo check`
+# / `cargo test` aren't asked to honour an MSVC-specific link arg.
+[target.x86_64-pc-windows-msvc]
+rustflags = [
+ "-C", "link-arg=/DELAYLOAD:WebView2Loader.dll",
+ "-C", "link-arg=delayimp.lib",
+]
diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore
new file mode 100644
index 0000000..b21bd68
--- /dev/null
+++ b/src-tauri/.gitignore
@@ -0,0 +1,7 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Generated by Tauri
+# will have schema files for capabilities auto-completion
+/gen/schemas
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
new file mode 100644
index 0000000..42d8fdc
--- /dev/null
+++ b/src-tauri/Cargo.lock
@@ -0,0 +1,6721 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-compression"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "beanfun"
+version = "6.0.0"
+dependencies = [
+ "aes",
+ "anyhow",
+ "assert_matches",
+ "axum",
+ "base64 0.22.1",
+ "cbc",
+ "chrono",
+ "cipher",
+ "des",
+ "html-escape",
+ "indexmap 2.14.0",
+ "md-5",
+ "nrbf",
+ "open",
+ "percent-encoding",
+ "pretty_assertions",
+ "quick-xml 0.37.5",
+ "rand 0.8.5",
+ "regex",
+ "reqwest 0.12.28",
+ "reqwest_cookie_store",
+ "serde",
+ "serde_json",
+ "sha2",
+ "specta",
+ "specta-typescript",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-dialog",
+ "tauri-plugin-opener",
+ "tauri-specta",
+ "tempfile",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-test",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "windows 0.58.0",
+ "winreg 0.52.0",
+ "wiremock",
+ "wmi",
+ "zeroize",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.11.1",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "compression-codecs"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
+dependencies = [
+ "compression-core",
+ "flate2",
+ "memchr",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "log",
+ "publicsuffix",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "log",
+ "publicsuffix",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.29.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "matches",
+ "phf 0.10.1",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf 0.13.1",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "deadpool"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
+dependencies = [
+ "deadpool-runtime",
+ "lazy_static",
+ "num_cpus",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "des"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+dependencies = [
+ "bit-set",
+ "cssparser 0.36.0",
+ "foldhash 0.2.0",
+ "html5ever 0.38.0",
+ "precomputed-hash",
+ "selectors 0.36.1",
+ "tendril 0.5.0",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "embed-resource"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 0.9.12+spec-1.1.0",
+ "vswhom",
+ "winreg 0.55.0",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.11.1",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html-escape"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever 0.14.1",
+ "match_token",
+]
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever 0.38.0",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.0",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.11.1",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kuchikiki"
+version = "0.8.8-speedreader"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
+dependencies = [
+ "cssparser 0.29.6",
+ "html5ever 0.29.1",
+ "indexmap 2.14.0",
+ "selectors 0.24.0",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.185"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
+dependencies = [
+ "log",
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+ "string_cache 0.8.9",
+ "string_cache_codegen 0.5.4",
+ "tendril 0.4.3",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril 0.5.0",
+ "web_atoms",
+]
+
+[[package]]
+name = "match_token"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.11.1",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nrbf"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac2270c96e013b136a3618ae2702ebb40de4d668ec965a287be8498dbbbba540"
+dependencies = [
+ "bitflags 2.11.1",
+ "nom",
+ "rust_decimal",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.11.1",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.11.1",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "open"
+version = "5.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
+dependencies = [
+ "dunce",
+ "is-wsl",
+ "libc",
+ "pathdiff",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_macros 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher 1.0.2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plist"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.14.0",
+ "quick-xml 0.38.4",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.11+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "psl-types"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
+
+[[package]]
+name = "publicsuffix"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
+dependencies = [
+ "idna",
+ "psl-types",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.11.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "cookie",
+ "cookie_store 0.22.1",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "reqwest_cookie_store"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2314c325724fea278d44c13a525ebf60074e33c05f13b4345c076eb65b2446b3"
+dependencies = [
+ "bytes",
+ "cookie_store 0.21.1",
+ "reqwest 0.12.28",
+ "url",
+]
+
+[[package]]
+name = "rfd"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
+dependencies = [
+ "block2",
+ "dispatch2",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
+dependencies = [
+ "arrayvec",
+ "num-traits",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.11.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser 0.29.6",
+ "derive_more 0.99.20",
+ "fxhash",
+ "log",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc 0.2.0",
+ "smallvec",
+]
+
+[[package]]
+name = "selectors"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+dependencies = [
+ "bitflags 2.11.1",
+ "cssparser 0.36.0",
+ "derive_more 2.1.1",
+ "log",
+ "new_debug_unreachable",
+ "phf 0.13.1",
+ "phf_codegen 0.13.1",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc 0.4.3",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "specta"
+version = "2.0.0-rc.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971"
+dependencies = [
+ "paste",
+ "serde_json",
+ "specta-macros",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "specta-macros"
+version = "2.0.0-rc.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517"
+dependencies = [
+ "Inflector",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "specta-serde"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63"
+dependencies = [
+ "specta",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "specta-typescript"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d"
+dependencies = [
+ "specta",
+ "specta-serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.11.3",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.13.1",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.34.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20"
+dependencies = [
+ "bitflags 2.11.1",
+ "block2",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "once_cell",
+ "parking_lot",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest 0.13.2",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "specta",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "toml 0.9.12+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.117",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "toml 0.9.12+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-dialog"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809"
+dependencies = [
+ "log",
+ "raw-window-handle",
+ "rfd",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-plugin-fs",
+ "thiserror 2.0.18",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-fs"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "glob",
+ "log",
+ "objc2-foundation",
+ "percent-encoding",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "toml 0.9.12+spec-1.1.0",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-opener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f"
+dependencies = [
+ "dunce",
+ "glob",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "open",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.18",
+ "url",
+ "windows 0.61.3",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.61.3",
+ "wry",
+]
+
+[[package]]
+name = "tauri-specta"
+version = "2.0.0-rc.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655"
+dependencies = [
+ "heck 0.5.0",
+ "serde",
+ "serde_json",
+ "specta",
+ "specta-typescript",
+ "tauri",
+ "tauri-specta-macros",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "tauri-specta-macros"
+version = "2.0.0-rc.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dunce",
+ "glob",
+ "html5ever 0.29.1",
+ "http",
+ "infer",
+ "json-patch",
+ "kuchikiki",
+ "log",
+ "memchr",
+ "phf 0.11.3",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.18",
+ "toml 0.9.12+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
+dependencies = [
+ "new_debug_unreachable",
+ "utf-8",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-test"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545"
+dependencies = [
+ "futures-core",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.11+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.1",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow 1.0.1",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "async-compression",
+ "bitflags 2.11.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "iri-string",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.14.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.1",
+ "hashbrown 0.15.5",
+ "indexmap 2.14.0",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web_atoms"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576"
+dependencies = [
+ "phf 0.13.1",
+ "phf_codegen 0.13.1",
+ "string_cache 0.9.0",
+ "string_cache_codegen 0.6.1",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+dependencies = [
+ "thiserror 2.0.18",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+dependencies = [
+ "windows-core 0.58.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
+dependencies = [
+ "windows-core 0.59.0",
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+dependencies = [
+ "windows-implement 0.58.0",
+ "windows-interface 0.58.0",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
+dependencies = [
+ "windows-implement 0.59.0",
+ "windows-interface 0.59.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.3.1",
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result 0.2.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wiremock"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
+dependencies = [
+ "assert-json-diff",
+ "base64 0.22.1",
+ "deadpool",
+ "futures",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.14.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.1",
+ "indexmap 2.14.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.14.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "wmi"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7787dacdd8e71cbc104658aade4009300777f9b5fda6a75f19145fedb8a18e71"
+dependencies = [
+ "chrono",
+ "futures",
+ "log",
+ "serde",
+ "thiserror 2.0.18",
+ "windows 0.59.0",
+ "windows-core 0.59.0",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "wry"
+version = "0.54.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.15",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
+dependencies = [
+ "serde",
+ "winnow 0.7.15",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.15",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.117",
+ "winnow 0.7.15",
+]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
new file mode 100644
index 0000000..21a5e96
--- /dev/null
+++ b/src-tauri/Cargo.toml
@@ -0,0 +1,161 @@
+[package]
+name = "beanfun"
+version = "6.0.0"
+description = "Beanfun — Rust/Tauri reimplementation of Beanfun launcher"
+authors = ["Pungin", "YCC3741"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+# The `_lib` suffix may seem redundant but it is necessary
+# to make the lib name unique and wouldn't conflict with the bin name.
+# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
+name = "beanfun_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+# SHA-256 hashing of LocaleRemulator blobs at build time — result is
+# emitted into `$OUT_DIR/lr_sha256.rs` and `include!`'d by the
+# `services::game::locale_remulator` module so runtime integrity
+# checks need no hex-string parsing. Mirrors the runtime `sha2` version
+# to avoid double-compiling the crate (P8 chunk 8.2).
+sha2 = "0.10"
+
+[dependencies]
+# Tauri core — `specta` feature wires Tauri's State / Window types into
+# the specta type-graph so `tauri-specta` can derive them. Required by
+# `tauri-specta` 2.0.0-rc.x (P10 chunk 10.1).
+tauri = { version = "2", features = ["specta", "tray-icon"] }
+tauri-plugin-opener = "2"
+# Native file picker (open / save dialogs) — used by `pages/ManageAccount.vue`
+# (P12.2 D9) for plaintext Users.dat import / export, mirroring the WPF
+# `Microsoft.Win32.OpenFileDialog` / `SaveFileDialog` calls in the legacy
+# AccountManager flow. Implementation is the cross-platform `rfd` crate
+# under the hood (Win32 IFileOpenDialog on Windows, AppKit on macOS, Zenity
+# on Linux); no extra config needed beyond declaring the plugin in `lib.rs`
+# and granting `dialog:default` in `capabilities/default.json`.
+tauri-plugin-dialog = "2"
+
+# Cross-platform URL / file opener (ShellExecuteW on Windows,
+# LSOpenCFURLRef on macOS, xdg-open on Linux). Already pulled in as
+# a transitive dep via tauri-plugin-opener; declared here directly
+# so `services::system::open_url` can call `open::that` without
+# depending on `AppHandle` (the plugin's Rust API requires one,
+# which would tie the service layer to the Tauri runtime and break
+# the "services are framework-agnostic" invariant set in P10.1).
+open = "5"
+
+# IPC type generation — pin to `tauri-specta` rc.21 (2025-01-13) instead
+# of the newest rc.24 (2026-03-30): rc.24 pulls `specta` rc.24 which uses
+# the `#![feature(debug_closure_helpers)]`-gated `fmt::from_fn`, breaking
+# stable-Rust builds (rust-lang/rust#117729 not stabilized yet). rc.21
+# pairs with `specta =2.0.0-rc.22` + `specta-typescript =0.0.9` (all pre-
+# `fmt::from_fn` churn) and compiles cleanly on `rustc 1.92.0 stable`.
+# Revisit when `debug_closure_helpers` stabilizes (PR rust-lang/rust#146099).
+tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
+specta = { version = "=2.0.0-rc.22", features = ["derive", "serde_json"] }
+specta-typescript = "=0.0.9"
+
+# Serialization
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+# Error handling
+thiserror = "2"
+anyhow = "1"
+
+# Logging / tracing
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+
+# HTTP client (rustls — avoid OpenSSL dependency)
+reqwest = { version = "0.12", default-features = false, features = [
+ "json",
+ "cookies",
+ "rustls-tls",
+ "gzip",
+ "deflate",
+] }
+reqwest_cookie_store = "0.8"
+
+# Async runtime
+tokio = { version = "1", features = ["rt", "sync", "time", "fs", "macros"] }
+
+# Crypto
+des = "0.8"
+# `block-padding` feature exposes the `Pkcs7` padding type; `alloc` feature
+# enables `BlockEncryptMut::encrypt_padded_vec_mut::` / `BlockDecryptMut::
+# decrypt_padded_vec_mut` (the heap-allocating variants). Both are required by
+# `services::storage::aes_backup` for WPF AES backup wire format.
+cipher = { version = "0.4", features = ["block-padding", "alloc"] }
+sha2 = "0.10"
+# AES-128-CBC + PKCS7 + MD5 derived key/IV — wire-format compatible with
+# WPF `Beanfun/Windows/AccRecovery.xaml.cs` AES backup format. MD5 is
+# weak-by-modern-standards but the legacy WPF clients ship millions of
+# `Users.dat` backups in this format; matching it byte-for-byte preserves
+# cross-machine portability for users migrating off the legacy launcher.
+# Threat model is *portability*, not confidentiality (P12.2 D10 Q3).
+md-5 = "0.10"
+aes = "0.8"
+cbc = "0.1"
+
+# Parsing / text
+quick-xml = { version = "0.37", features = ["serialize"] }
+regex = "1"
+url = "2"
+percent-encoding = "2"
+base64 = "0.22"
+chrono = { version = "0.4", features = ["serde"] }
+html-escape = "0.2"
+indexmap = "2"
+
+# MS-NRBF (.NET BinaryFormatter) reader — legacy Users.dat migration (P6)
+nrbf = "0.2"
+
+# Secret handling — zero password / token buffers on drop
+zeroize = { version = "1", features = ["derive"] }
+
+# Random number generation (storage entropy salt — OsRng only; CSPRNG)
+rand = "0.8"
+
+[target.'cfg(windows)'.dependencies]
+# Win32 API bindings — features will be expanded per phase (DPAPI in P5, WMI/PostMessage in P8/P9).
+windows = { version = "0.58", features = [
+ "Win32_Foundation",
+ "Win32_Globalization",
+ "Win32_Graphics_Gdi",
+ "Win32_Security",
+ "Win32_Security_Cryptography",
+ "Win32_System_Threading",
+ "Win32_System_ProcessStatus",
+ "Win32_UI_Input_KeyboardAndMouse",
+ "Win32_UI_WindowsAndMessaging",
+ "Win32_UI_Shell",
+] }
+winreg = "0.52"
+wmi = "0.14"
+
+[dev-dependencies]
+wiremock = "0.6"
+axum = "0.8"
+assert_matches = "1"
+tempfile = "3"
+pretty_assertions = "1"
+tokio-test = "0.4"
+
+[features]
+# Test-only NRBF byte-stream builder. Gated behind a feature flag so
+# the fixture helpers (~300 lines in `core::legacy::nrbf::fixture`)
+# stay out of production binaries. Enabled automatically during unit
+# tests via `cfg(test)`; integration tests (`tests/storage_legacy.rs`)
+# must opt in via `cargo test --features test-fixtures`.
+test-fixtures = []
+
+# Integration tests that rely on the NRBF fixture builder.
+# `cargo test` (no flags) will skip this target silently —
+# see module doc in `tests/storage_legacy.rs` for run instructions.
+[[test]]
+name = "storage_legacy"
+required-features = ["test-fixtures"]
diff --git a/Beanfun/LocaleRemulator/LRConfig.xml b/src-tauri/LocaleRemulator/LRConfig.xml
similarity index 100%
rename from Beanfun/LocaleRemulator/LRConfig.xml
rename to src-tauri/LocaleRemulator/LRConfig.xml
diff --git a/Beanfun/LocaleRemulator/LRHookx32.dll b/src-tauri/LocaleRemulator/LRHookx32.dll
similarity index 100%
rename from Beanfun/LocaleRemulator/LRHookx32.dll
rename to src-tauri/LocaleRemulator/LRHookx32.dll
diff --git a/Beanfun/LocaleRemulator/LRHookx64.dll b/src-tauri/LocaleRemulator/LRHookx64.dll
similarity index 100%
rename from Beanfun/LocaleRemulator/LRHookx64.dll
rename to src-tauri/LocaleRemulator/LRHookx64.dll
diff --git a/Beanfun/LocaleRemulator/LRProc.exe b/src-tauri/LocaleRemulator/LRProc.exe
similarity index 100%
rename from Beanfun/LocaleRemulator/LRProc.exe
rename to src-tauri/LocaleRemulator/LRProc.exe
diff --git a/Beanfun/LocaleRemulator/LRSubMenus.dll b/src-tauri/LocaleRemulator/LRSubMenus.dll
similarity index 100%
rename from Beanfun/LocaleRemulator/LRSubMenus.dll
rename to src-tauri/LocaleRemulator/LRSubMenus.dll
diff --git a/src-tauri/build.rs b/src-tauri/build.rs
new file mode 100644
index 0000000..e65f536
--- /dev/null
+++ b/src-tauri/build.rs
@@ -0,0 +1,177 @@
+use std::path::PathBuf;
+
+use sha2::{Digest, Sha256};
+
+/// LocaleRemulator assets shipped by the WPF tree, in the exact order
+/// `MainWindow::startByLR` (L1904-1914) checks them. The runtime
+/// `locale_remulator` module `include_bytes!`s the same files with the
+/// same ordering, so this list is the single source of truth.
+const LR_ASSETS: &[&str] = &[
+ "LRConfig.xml",
+ "LRHookx32.dll",
+ "LRHookx64.dll",
+ "LRProc.exe",
+ "LRSubMenus.dll",
+];
+
+fn main() {
+ let attributes = tauri_build::Attributes::new();
+ #[cfg(windows)]
+ let attributes = {
+ embed_app_manifest_for_all_binaries();
+ attributes.windows_attributes(tauri_build::WindowsAttributes::new_without_app_manifest())
+ };
+ tauri_build::try_build(attributes).expect("tauri_build::try_build failed");
+
+ emit_lr_sha256();
+}
+
+/// Embed the Windows application manifest into **every** binary
+/// produced by this crate (the main app, examples, and test
+/// executables) instead of just the main `Beanfun.exe`.
+///
+/// # Why this exists (P10.3 D6)
+///
+/// `tauri-build`'s default Windows manifest path goes through
+/// [`tauri_winres::WindowsResource::set_manifest`] →
+/// `embed_resource::compile()`, which emits a `cargo:rustc-link-arg-bins`
+/// directive. The `-bins` suffix scopes the linker arg to *bin*
+/// targets only — example binaries (`cargo run --example
+/// export_bindings`) and test binaries (`cargo test --lib`) are
+/// excluded. Those binaries still get the **import** for
+/// Common Controls v6 APIs (because the `tauri` rlib on the link
+/// line carries a static dependency on `comctl32.dll` v6 entries),
+/// but without a manifest declaring the Common Controls v6
+/// ``, Windows resolves `comctl32.dll` to the
+/// stub v5 redirector that lacks those v6-only exports — so the
+/// loader bails with `STATUS_ENTRYPOINT_NOT_FOUND` (0xc0000139)
+/// at process-start time.
+///
+/// Tauri tracks this as a known issue across several reports
+/// (tauri-apps/tauri#11028 / #13419 / #13948 / #14580); the
+/// official workaround — recommended by Tauri maintainer
+/// `lucasfernog` — is exactly what this function does:
+///
+/// 1. Tell `tauri-build` to skip the default manifest embed via
+/// [`tauri_build::WindowsAttributes::new_without_app_manifest`]
+/// (otherwise the main binary would end up with two competing
+/// manifests, and the linker emits `LNK4078` warnings).
+/// 2. Re-embed the same manifest ourselves through
+/// `cargo:rustc-link-arg=/MANIFEST:EMBED` +
+/// `/MANIFESTINPUT:` — `rustc-link-arg` (no `-bins`
+/// suffix) propagates to **every** linker invocation in this
+/// crate, so example and test binaries inherit the manifest
+/// too.
+///
+/// The manifest content under
+/// `src-tauri/windows-app-manifest.xml` is byte-identical to the
+/// `tauri-build`-bundled `windows-app-manifest.xml` — we copied
+/// it verbatim so production binaries see the exact same
+/// Common Controls v6 dependency declaration they did before this
+/// change. Other Windows resources `tauri-build` injects
+/// (version info, icon, product name) are unaffected and continue
+/// to land on the main binary only via `tauri-build`'s separate
+/// `WindowsResource` call.
+///
+/// # Linker requirements
+///
+/// `/MANIFEST:EMBED` requires `mt.exe` (Windows SDK Manifest
+/// Tool) on `PATH` for the linker to call. The MSVC toolchain
+/// ships `mt.exe` alongside `link.exe`, so any developer with
+/// MSVC build tools installed (a hard requirement for compiling
+/// Tauri on Windows anyway) already has it.
+#[cfg(windows)]
+fn embed_app_manifest_for_all_binaries() {
+ static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml";
+
+ let manifest = PathBuf::from(
+ std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is always set by cargo"),
+ )
+ .join(WINDOWS_MANIFEST_FILE);
+
+ println!("cargo:rerun-if-changed={}", manifest.display());
+ println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
+
+ let profile = std::env::var("PROFILE").unwrap_or_default();
+ if profile == "release" {
+ println!("cargo:rustc-link-arg=/MANIFESTUAC:level='highestAvailable' uiAccess='false'");
+ }
+
+ println!(
+ "cargo:rustc-link-arg=/MANIFESTINPUT:{}",
+ manifest
+ .to_str()
+ .expect("manifest path is always valid UTF-8 on Windows host")
+ );
+}
+
+/// Compute the SHA-256 of every LocaleRemulator asset referenced by
+/// `LR_ASSETS` and write a Rust source snippet to `$OUT_DIR/lr_sha256.rs`
+/// so the runtime module can `include!` a typed const array.
+///
+/// # Panics
+///
+/// Build fails if any asset is missing: without the hash we can't
+/// enforce the SHA-256 integrity check P8 chunk 8.2 ships as an
+/// upgrade over WPF's length-only comparison, and silently skipping
+/// would let a tampered DLL sneak through.
+fn emit_lr_sha256() {
+ let manifest_dir = PathBuf::from(
+ std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is always set by cargo"),
+ );
+ let lr_dir = manifest_dir.join("LocaleRemulator");
+
+ println!("cargo:rerun-if-changed=build.rs");
+
+ let mut entries: Vec<(String, [u8; 32])> = Vec::with_capacity(LR_ASSETS.len());
+ for name in LR_ASSETS {
+ let path = lr_dir.join(name);
+ println!("cargo:rerun-if-changed={}", path.display());
+
+ let bytes = std::fs::read(&path).unwrap_or_else(|err| {
+ panic!(
+ "LocaleRemulator asset `{name}` is required for the runtime integrity check \
+ but could not be read from `{}`: {err}. The WPF tree at Beanfun/LocaleRemulator/ \
+ must contain this file for Beanfun to build.",
+ path.display()
+ )
+ });
+
+ let digest: [u8; 32] = Sha256::digest(&bytes).into();
+ entries.push((name.to_string(), digest));
+ }
+
+ let out_dir = PathBuf::from(
+ std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo for build scripts"),
+ );
+ let out_file = out_dir.join("lr_sha256.rs");
+ std::fs::write(&out_file, render_sha256_table(&entries))
+ .unwrap_or_else(|err| panic!("failed to write `{}`: {err}", out_file.display()));
+}
+
+/// Render the computed `(name, sha256)` pairs into a Rust source
+/// snippet that the runtime module `include!`s. Kept as a separate
+/// pure function so the format stays reviewable in one place.
+fn render_sha256_table(entries: &[(String, [u8; 32])]) -> String {
+ let mut out = String::new();
+ out.push_str("// @generated by build.rs — LocaleRemulator SHA-256 table.\n");
+ out.push_str("// Do not edit; regenerate by rebuilding with updated binaries under\n");
+ out.push_str("// `LocaleRemulator/`.\n\n");
+ out.push_str("pub(crate) const LR_SHA256: [(&str, [u8; 32]); ");
+ out.push_str(&entries.len().to_string());
+ out.push_str("] = [\n");
+ for (name, digest) in entries {
+ out.push_str(" (\"");
+ out.push_str(name);
+ out.push_str("\", [");
+ for (idx, byte) in digest.iter().enumerate() {
+ if idx > 0 {
+ out.push_str(", ");
+ }
+ out.push_str(&format!("0x{byte:02x}"));
+ }
+ out.push_str("]),\n");
+ }
+ out.push_str("];\n");
+ out
+}
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..803b0a6
--- /dev/null
+++ b/src-tauri/capabilities/default.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Capability for the main window",
+ "windows": ["main"],
+ "permissions": ["core:default", "opener:default", "dialog:default"]
+}
diff --git a/src-tauri/clippy.toml b/src-tauri/clippy.toml
new file mode 100644
index 0000000..cf04940
--- /dev/null
+++ b/src-tauri/clippy.toml
@@ -0,0 +1,3 @@
+msrv = "1.80"
+cognitive-complexity-threshold = 25
+too-many-arguments-threshold = 7
diff --git a/src-tauri/examples/export_bindings.rs b/src-tauri/examples/export_bindings.rs
new file mode 100644
index 0000000..ecd3a3e
--- /dev/null
+++ b/src-tauri/examples/export_bindings.rs
@@ -0,0 +1,86 @@
+//! Standalone `bindings.ts` regenerator.
+//!
+//! Run via:
+//!
+//! ```text
+//! cargo run --example export_bindings
+//! ```
+//!
+//! from `src-tauri/`. Writes the regenerated
+//! `bindings.ts` to the canonical location resolved by
+//! [`beanfun_lib::default_bindings_path`] (i.e.
+//! `src/types/bindings.ts`).
+//!
+//! # Why a dedicated example instead of `cargo tauri dev`?
+//!
+//! `cargo tauri dev` also regenerates `bindings.ts` on every debug
+//! boot (see [`beanfun_lib::run`]'s `export_specta_bindings`
+//! call), but it also:
+//!
+//! - spins up Vite's frontend dev server,
+//! - launches a WebView2 window,
+//! - blocks the terminal until the user closes the window.
+//!
+//! That's overkill when the only thing you need is a refreshed
+//! `bindings.ts` after editing a command signature. This example
+//! bypasses all the UI machinery and exits as soon as the file is
+//! written — typical wall-clock time is a couple of seconds on a
+//! warm build.
+//!
+//! # Shared plumbing with `run()`
+//!
+//! Target path comes from [`beanfun_lib::default_bindings_path`]
+//! — the same helper [`beanfun_lib::run`]'s debug-boot exporter
+//! calls, so this binary and the live app can never disagree on
+//! where `bindings.ts` lives. The [`beanfun_lib::commands::build_specta_builder`]
+//! helper is likewise the single source of truth for which commands
+//! get exported, so drift between runtime dispatch and emitted TS
+//! is impossible by construction. The TypeScript exporter (header
+//! injection, comment style, formatter) comes from
+//! [`beanfun_lib::default_typescript_exporter`] so this binary
+//! and the dev-mode auto-export emit byte-identical output.
+//!
+//! # Runtime type parameter
+//!
+//! Instantiates [`build_specta_builder`] with `tauri::Wry` (the
+//! production runtime) so the emitted TS exactly matches what
+//! `cargo tauri dev` would produce on the next boot. Swapping in
+//! `tauri::test::MockRuntime` would re-link `tauri-runtime-wry`
+//! anyway (via `tauri-specta`'s transitive deps on Windows — see
+//! the module docs on `commands::bindings_file_tests` for the
+//! `webview2-com-sys` linkage analysis) and would not meaningfully
+//! shrink the build closure, so the MockRuntime detour buys
+//! nothing here.
+//!
+//! # Exit codes
+//!
+//! - `0` — success.
+//! - `1` — `tauri_specta::Builder::export` returned an error (TS
+//! emission failed or the target path couldn't be written to).
+//! The error is printed to stderr with the target path so CI
+//! logs pinpoint the failure cause.
+//!
+//! Uses `std::process::exit` rather than propagating through
+//! `main() -> Result<_, _>` so the stderr line stays free of the
+//! default `Error:` prefix `?` would inject — keeping the output
+//! consistent with the existing `export_specta_bindings` stderr
+//! format in `lib.rs`.
+
+use beanfun_lib::{
+ commands::build_specta_builder, default_bindings_path, default_typescript_exporter,
+};
+
+fn main() {
+ let builder = build_specta_builder::();
+ let target = default_bindings_path();
+
+ if let Err(err) = builder.export(default_typescript_exporter(), &target) {
+ eprintln!(
+ "export_bindings: tauri-specta export failed: {err} (target={})",
+ target.display()
+ );
+ std::process::exit(1);
+ }
+
+ println!("export_bindings: wrote {}", target.display());
+}
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
new file mode 100644
index 0000000..93bf62c
Binary files /dev/null and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
new file mode 100644
index 0000000..79cbd4e
Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
new file mode 100644
index 0000000..10e4967
Binary files /dev/null and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png
new file mode 100644
index 0000000..3ed3c92
Binary files /dev/null and b/src-tauri/icons/64x64.png differ
diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..3d59f68
Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ
diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..58839fd
Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ
diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..7bd3ae5
Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ
diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..35a3ed9
Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ
diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..4c48e57
Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ
diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..ecd5b56
Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ
diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..a51745f
Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ
diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..f718ee6
Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ
diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..20ae4eb
Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ
diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..4b61722
Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ
diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..2ffbf24
--- /dev/null
+++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..650695d
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..363e775
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..ba86e8e
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..668839c
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..aabe0be
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..6dcc90b
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..037881b
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..33a423a
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c52e9cb
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f465da9
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..ed52d98
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..de9927b
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..22e05cd
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..bcd99b7
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e8d9ca0
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml
new file mode 100644
index 0000000..ea9c223
--- /dev/null
+++ b/src-tauri/icons/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #fff
+
\ No newline at end of file
diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns
new file mode 100644
index 0000000..5396b9e
Binary files /dev/null and b/src-tauri/icons/icon.icns differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
new file mode 100644
index 0000000..3e3a75e
Binary files /dev/null and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
new file mode 100644
index 0000000..6a50b77
Binary files /dev/null and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png
new file mode 100644
index 0000000..47f2836
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
new file mode 100644
index 0000000..a021727
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png
new file mode 100644
index 0000000..a021727
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png
new file mode 100644
index 0000000..4a15b4a
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png
new file mode 100644
index 0000000..09e0113
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
new file mode 100644
index 0000000..822aa90
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png
new file mode 100644
index 0000000..822aa90
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png
new file mode 100644
index 0000000..2847d19
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png
new file mode 100644
index 0000000..a021727
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
new file mode 100644
index 0000000..da15207
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png
new file mode 100644
index 0000000..da15207
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png
new file mode 100644
index 0000000..70c08ab
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png
new file mode 100644
index 0000000..292494b
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png
new file mode 100644
index 0000000..70c08ab
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png
new file mode 100644
index 0000000..cee7cf9
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png
new file mode 100644
index 0000000..74c01f7
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png
new file mode 100644
index 0000000..2d0e732
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 0000000..8338fa9
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ
diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs
new file mode 100644
index 0000000..b5f0124
--- /dev/null
+++ b/src-tauri/src/commands/account.rs
@@ -0,0 +1,1089 @@
+//! Account commands — service-account management for the logged-in
+//! Beanfun session.
+//!
+//! # Families exposed in P10.2
+//!
+//! | Command | Family | Purpose |
+//! |-------------------------|--------------|------------------------------------------------------------------|
+//! | [`get_accounts`] | base | Fetch the sorted service-account list + quota notice |
+//! | [`refresh`] | base | Semantic alias — re-runs the same flow as [`get_accounts`] |
+//! | `add_service_account` | management | (D9) Add a connected-game service account |
+//! | `change_display_name` | management | (D9) Rename a service account |
+//! | `get_contract` | info | (D10) Fetch service contract URL |
+//! | `get_email` | info | (D10) Fetch account email |
+//! | `get_remain_point` | info | (D10) Fetch remaining Beanfun points |
+//!
+//! The `unconnected_game_*` family (unconnected-game flows) is
+//! **deferred to P12** — they're UI-driven (captcha prompt, display
+//! name picker, password change wizard) and their command shape
+//! depends on the Vue UX (P10.2 pre-flight Q7 = A).
+//!
+//! # Session gating
+//!
+//! Every command in this module is **session-required** — they
+//! start by calling [`commands::session::require_auth`] which
+//! surfaces `auth.session_required` when no login is active. The
+//! shared [`list_accounts_internal`] helper below centralises the
+//! auth check + service dispatch so `get_accounts` and `refresh`
+//! stay a single line apiece.
+//!
+//! # DTO policy (P10.2 Q4 = C)
+//!
+//! [`ServiceAccount`], [`AccountListResult`], and
+//! [`AmountLimitNotice`][crate::services::beanfun::AmountLimitNotice]
+//! are **pure data types** — no secrets, no binary blobs — so they
+//! derive `serde::Serialize + specta::Type` directly on the
+//! service-layer struct/enum (not a shadow DTO). The command layer
+//! returns them by value and `tauri-specta` emits a matching
+//! TypeScript type into `bindings.ts`. Field names are WPF-verbatim
+//! (`sid` / `ssn` / `sname` / …) because keeping the Rust ↔ WPF
+//! mapping 1:1 outweighs the TypeScript style win of `camelCase`.
+//!
+//! [`commands::session::require_auth`]: crate::commands::session::require_auth
+
+use tauri::State;
+
+use crate::commands::{error::CommandError, session::require_auth, state::AppState};
+use crate::services::beanfun::{
+ add_service_account as service_add_service_account,
+ change_service_account_display_name as service_change_display_name,
+ get_accounts as service_get_accounts, get_email as service_get_email,
+ get_remain_point as service_get_remain_point,
+ get_service_contract as service_get_service_contract,
+ unconnected_game_add_account as service_unconnected_game_add_account,
+ unconnected_game_add_account_check as service_unconnected_game_add_account_check,
+ unconnected_game_add_account_check_nickname as service_unconnected_game_add_account_check_nickname,
+ unconnected_game_change_password as service_unconnected_game_change_password,
+ unconnected_game_init_add_account_payload as service_unconnected_game_init_add_account_payload,
+ AccountListResult, AddAccountInit, AddAccountOutcome, AddAccountSession, ChangePasswordOutcome,
+ CheckOutcome, ServiceAccount,
+};
+
+/// Internal helper shared by [`get_accounts`] and [`refresh`].
+///
+/// Unwinds the auth check + service-layer dispatch so each public
+/// command body collapses to a one-liner — single source of truth
+/// for "how do we fetch the current session's account list?". If
+/// a future tweak to the flow is needed (e.g. bypass cache, force
+/// cookie refresh, swap service provider), this is the only place
+/// the change lands.
+///
+/// # Why `require_auth` clones the client + session
+///
+/// The helper returns owned [`BeanfunClient`][bc] + [`Session`][sesh]
+/// values so the [`AppState::auth`] read guard can drop before the
+/// HTTP `get_accounts` call begins. Holding a guard across `.await`
+/// would block the `logout` command from acquiring its write guard
+/// concurrently.
+///
+/// [bc]: crate::services::beanfun::BeanfunClient
+/// [sesh]: crate::services::beanfun::Session
+async fn list_accounts_internal(state: &AppState) -> Result {
+ let (client, session) = require_auth(state).await?;
+ let result = service_get_accounts(
+ &client,
+ &session,
+ &session.service_code,
+ &session.service_region,
+ )
+ .await?;
+ Ok(result)
+}
+
+/// Update the **active service code / region** on the live
+/// [`Session`][sn] so subsequent session-gated commands target the
+/// game the user just picked from `windows/GameList.vue`.
+///
+/// # Why a backend command (not a frontend-only Pinia mutation)?
+///
+/// Every other account-family command (`get_accounts` /
+/// `add_service_account` / `change_display_name` /
+/// `unconnected_game_*` / `get_contract` / etc.) deliberately pulls
+/// `service_code` / `service_region` off the in-memory
+/// [`Session`][sn] inside the command body — the IPC contract
+/// **does not** accept the pair as a parameter, on the
+/// "single-source-of-truth" rationale documented in
+/// [`add_service_account`]'s docblock. That contract works for the
+/// post-login first paint (the login flow seeds the session with the
+/// region's default game) but breaks the moment the user picks a
+/// different game from the picker dialog: the frontend's
+/// `useGameStore.selectedGameCode` updates, but the backend session
+/// keeps pointing at the original game, and the next
+/// `get_accounts` / `add_service_account` round-trip silently
+/// queries the wrong service.
+///
+/// WPF avoided this entirely because `MainWindow.service_code`
+/// **is** the source of truth — there's no separate per-call
+/// session struct, the field is mutated in place by
+/// `MainWindow.GameList.SelectionChanged` (mirrored at L661 / L520
+/// / L523 of `MainWindow.xaml.cs`) before the next
+/// `bfClient.GetAccounts(service_code, service_region)` runs (L638
+/// of the same file). Re-introducing that mutability on the SPA
+/// backend keeps the IPC contract intact while preserving WPF's
+/// "switching games re-targets every subsequent service call"
+/// semantic.
+///
+/// # Contract
+///
+/// - Acquires the [`AppState::auth`] **write** lock (rare path —
+/// only fires on game-picker confirmation, and the `tokio::sync::RwLock`
+/// queues writers behind any in-flight readers from concurrent
+/// commands).
+/// - Returns `auth.session_required` when no session is active
+/// (the picker is gated behind the AccountList route which
+/// itself is auth-required, but the defensive guard keeps the
+/// contract honest).
+/// - **Stateless on the wire**: empty input is rejected by the
+/// service-layer parser at the next `get_accounts` call, not
+/// here — this command is a pure swap and has no policy on
+/// what counts as a valid `(code, region)` pair (mirrors WPF's
+/// "any string the picker dialog supplies is fine; let the
+/// gamezone POST surface a server-side error if it's bogus"
+/// stance).
+///
+/// # Frontend usage
+///
+/// Called from `pages/AccountList.vue::onGameChanged` (P12.3 D8)
+/// inside the `` handler, **before** the
+/// follow-up `useAccountStore.refresh()` that paints the new
+/// game's account list. Persisting `loginGame` to `Config.xml`
+/// (so the choice survives a re-login) is a separate concern
+/// owned by the frontend (`useConfigStore.set('loginGame', ...)`)
+/// because Config.xml is a frontend-mediated cache (see
+/// `useConfigStore` docblock).
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+///
+/// [sn]: crate::services::beanfun::Session
+#[tauri::command]
+#[specta::specta]
+pub async fn set_active_service(
+ state: State<'_, AppState>,
+ service_code: String,
+ service_region: String,
+) -> Result<(), CommandError> {
+ set_active_service_internal(state.inner(), service_code, service_region).await
+}
+
+/// Tauri-State-free body of [`set_active_service`].
+///
+/// Split out from the `#[tauri::command]` wrapper for the same reason
+/// [`list_accounts_internal`] is — `tauri::State<'_, _>` cannot be
+/// constructed outside of `tauri::test::mock_app()`, so the unit
+/// tests in [`tests`] target this helper directly. The wrapper above
+/// is a one-line `state.inner()` adapter (no business logic), so a
+/// behavioural test on the helper covers the whole command surface
+/// without dragging the Tauri test runtime (and its WebView2
+/// dependency) into the test binary.
+async fn set_active_service_internal(
+ state: &AppState,
+ service_code: String,
+ service_region: String,
+) -> Result<(), CommandError> {
+ let mut guard = state.auth.write().await;
+ match guard.as_mut() {
+ Some(ctx) => {
+ ctx.session.service_code = service_code;
+ ctx.session.service_region = service_region;
+ Ok(())
+ }
+ None => Err(CommandError::new(
+ crate::commands::session::SESSION_REQUIRED_CODE,
+ "No active Beanfun session. Please log in and try again.",
+ )),
+ }
+}
+
+/// List the service accounts the logged-in user can launch into the
+/// session's current service + region.
+///
+/// # Returns
+///
+/// An [`AccountListResult`] bundle with:
+///
+/// - `accounts` — sorted by ascending `ssn` (WPF first-pass sort).
+/// - `amount_limit_notice` — typed quota-notice classification
+/// (`None` / `AuthReLoginRequired` / `Other { message }`).
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Every [`LoginError`][le] surfaced by the service-layer
+/// `get_accounts` (transport / parse / body-too-large). The
+/// P10.1 `From` impl handles mapping verbatim.
+///
+/// # Frontend usage
+///
+/// Called on first render of the account-picker screen. See
+/// [`refresh`] for the UI's "reload" affordance.
+///
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn get_accounts(state: State<'_, AppState>) -> Result {
+ list_accounts_internal(state.inner()).await
+}
+
+/// Semantic alias for [`get_accounts`] — re-fetch the account list.
+///
+/// # Why a second command instead of just `get_accounts`?
+///
+/// Two separate commands let the frontend's intent be legible at the
+/// call site (`invoke('get_accounts')` on first render vs.
+/// `invoke('refresh')` on the reload button) without the backend
+/// diverging behaviour. A future requirement (analytics counter,
+/// stricter rate limit, cache bypass) can land in one `#[tauri::command]`
+/// body without touching the other's contract.
+///
+/// # Implementation
+///
+/// Delegates to the same [`list_accounts_internal`] helper as
+/// [`get_accounts`] — both commands are pure wire adapters on top
+/// of the single internal primitive, so there is no duplicated
+/// flow logic to keep in sync (DRY).
+///
+/// # When to call
+///
+/// On user-initiated "refresh" button clicks, and after commands
+/// that invalidate the list (e.g. `add_service_account`,
+/// `change_display_name` — both in D9).
+#[tauri::command]
+#[specta::specta]
+pub async fn refresh(state: State<'_, AppState>) -> Result {
+ list_accounts_internal(state.inner()).await
+}
+
+/// Add a new service account (character slot) for the logged-in user
+/// under the session's current service + region.
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::add_service_account`][svc] verbatim:
+///
+/// - Empty `name` → `Ok(false)` *without firing a request* (server
+/// roundtrip is redundant — the form validation on the WPF dialog
+/// gates the same way, so we preserve both the UI semantic and the
+/// zero-network-cost shape).
+/// - Non-empty → `POST gamezone.ashx` with
+/// `strFunction=AddServiceAccount`; response's `intResult == 1` →
+/// `true`, anything else (including empty body or missing field) →
+/// `false`.
+///
+/// # Why pull `service_code` / `service_region` from the session?
+///
+/// WPF's `MainWindow.AddServiceAccount` (`Beanfun/MainWindow.xaml.cs`)
+/// uses the same globals — the add-account dialog only ever targets
+/// the user's current game. Exposing the two fields as IPC parameters
+/// would invite the frontend to pass mismatched values (e.g. a stale
+/// account-list snapshot from before the region switched), so we lock
+/// the source of truth to [`Session`][sesh] on the backend.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Any [`LoginError`][le] surfaced by the service (`auth.aspx`
+/// pre-flight / `gamezone.ashx` transport / JSON parse / body-too-
+/// large). Already mapped to `CommandError` by the P10.1
+/// `From` impl.
+///
+/// # Frontend usage
+///
+/// After a successful return, the caller should invoke [`refresh`]
+/// to pick up the new row (gamezone does not echo the account back).
+///
+/// [svc]: crate::services::beanfun::add_service_account
+/// [sesh]: crate::services::beanfun::Session
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn add_service_account(
+ state: State<'_, AppState>,
+ name: String,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let accepted = service_add_service_account(
+ &client,
+ &session,
+ &name,
+ &session.service_code,
+ &session.service_region,
+ )
+ .await?;
+ Ok(accepted)
+}
+
+/// Rename an existing service account's display name.
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::change_service_account_display_name`][svc]
+/// verbatim:
+///
+/// - `new_name.is_empty()` **or** `new_name == account.sname` →
+/// `Ok(false)` without firing a request (WPF early-out — server
+/// would reject identical names anyway, so we skip the roundtrip).
+/// - Otherwise → `POST gamezone.ashx` with
+/// `strFunction=ChangeServiceAccountDisplayName, sl=,
+/// said=, nsadn=`; response's
+/// `intResult == 1` → `true`, anything else → `false`.
+///
+/// # Why echo the whole `ServiceAccount` from the frontend?
+///
+/// The service layer mirrors WPF's signature (which takes the whole
+/// `ServiceAccount` so the call site can early-out on
+/// `newName == account.sname`). Rather than reshape the service
+/// call or build a partially-populated `ServiceAccount` in the
+/// command layer (which would require manually updating every time
+/// the struct gains a new field), we let the frontend echo the
+/// object it already has in hand from [`get_accounts`]. `ServiceAccount`
+/// contains only display-oriented public fields (no secrets), so
+/// the echo round-trip is safe — which is why it derives
+/// `serde::Deserialize` alongside `Serialize + specta::Type`.
+///
+/// # Why pull `game_code` from the session?
+///
+/// `game_code = "{service_code}_{service_region}"` — constructed on
+/// the backend to prevent the frontend from drifting the two halves
+/// against each other (exactly as [`add_service_account`] locks
+/// down the service code / region split).
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Any [`LoginError`][le] surfaced by the service.
+///
+/// # Frontend usage
+///
+/// On `Ok(true)`, the caller should update its local `ServiceAccount`
+/// (`sname = new_name`) or invoke [`refresh`]. On `Ok(false)` — either
+/// the caller passed an invalid / unchanged name (expected UI
+/// prevention), or the server rejected the change (show a generic
+/// "could not rename" message — mirrors WPF's `MsgChangeDisplayNameError`).
+///
+/// [svc]: crate::services::beanfun::change_service_account_display_name
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn change_display_name(
+ state: State<'_, AppState>,
+ new_name: String,
+ account: ServiceAccount,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let game_code = format!("{}_{}", session.service_code, session.service_region);
+ let accepted =
+ service_change_display_name(&client, &session, &new_name, &game_code, &account).await?;
+ Ok(accepted)
+}
+
+/// Fetch the EULA / service contract HTML for the session's current
+/// service + region.
+///
+/// # Contract
+///
+/// Thin wrapper over [`services::beanfun::get_service_contract`][svc].
+/// Same `service_code` / `service_region` policy as
+/// [`add_service_account`] — pulled from [`Session`][sesh] so the
+/// frontend cannot drift the two halves against each other.
+///
+/// Returns the raw HTML fragment the server emits in the
+/// `strResult` JSON field (or `""` when `intResult != 1` / the body
+/// is empty — matching WPF).
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Any [`LoginError`][le] surfaced by the service (transport,
+/// JSON parse, body-too-large).
+///
+/// # Frontend usage
+///
+/// The UI renders the returned HTML inside the "service contract"
+/// dialog (matching WPF's `Contract.xaml`). We return the body
+/// verbatim so the frontend's XSS policy — a dedicated render
+/// component with a sanitizer — owns the sanitisation decision;
+/// applying a sanitiser here would hard-code one policy for every
+/// consumer.
+///
+/// [svc]: crate::services::beanfun::get_service_contract
+/// [sesh]: crate::services::beanfun::Session
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn get_contract(state: State<'_, AppState>) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let contract = service_get_service_contract(
+ &client,
+ &session,
+ &session.service_code,
+ &session.service_region,
+ )
+ .await?;
+ Ok(contract)
+}
+
+/// Fetch the logged-in user's e-mail address.
+///
+/// # Contract
+///
+/// Thin wrapper over [`services::beanfun::get_email`][svc]. TW
+/// sessions return the captured address; HK sessions short-circuit
+/// to `""` **without** firing a request (the HK portal does not
+/// expose this endpoint — mirrors WPF `BeanfunClient.cs::getEmail`
+/// L245-246).
+///
+/// Returns the e-mail string, or `""` when the TW regex does not
+/// match / the session is HK.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Any [`LoginError`][le] surfaced by the service (transport,
+/// body-too-large).
+///
+/// # Frontend usage
+///
+/// The AccountList "view e-mail" affordance hides itself when the
+/// return is empty (matches WPF's `AccountList.xaml.cs`
+/// `m_GetEmail_Click` behaviour — nothing is shown for empty).
+///
+/// [svc]: crate::services::beanfun::get_email
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn get_email(state: State<'_, AppState>) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let email = service_get_email(&client, &session).await?;
+ Ok(email)
+}
+
+/// Fetch the remaining Beanfun points balance.
+///
+/// # Contract
+///
+/// Thin wrapper over [`services::beanfun::get_remain_point`][svc].
+/// Returns an `i32` for drop-in parity with WPF's `int` return
+/// (`BeanfunClient.cs::getRemainPoint` L214).
+///
+/// Returns `0` when the server response does not match the
+/// `"RemainPoint" : "…"` regex **or** the captured value is not a
+/// valid `i32` — matches WPF's blanket `catch { return 0; }`.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Any [`LoginError`][le] surfaced by the service. (The WPF
+/// `catch` would swallow these as `0`; we propagate so the
+/// frontend can distinguish "server rejected" from "network
+/// down" — the UI can apply the `→ 0` rule locally if strict
+/// WPF parity is desired.)
+///
+/// # Frontend usage
+///
+/// The AccountList header surfaces this as the "剩餘 B$" ticker
+/// (matches WPF `AccountList.xaml.cs` L139 → `updateRemainPoint`).
+///
+/// [svc]: crate::services::beanfun::get_remain_point
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn get_remain_point(state: State<'_, AppState>) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let pts = service_get_remain_point(&client, &session).await?;
+ Ok(pts)
+}
+
+// =============================================================================
+// P12.3 D3 — Unconnected-game account management
+// =============================================================================
+//
+// These five commands surface the 5-step "unconnected game" account
+// management flow that WPF wires into
+// `Windows/UnconnectedGame_AddAccount.xaml.cs` and
+// `Windows/UnconnectedGame_ChangePassword.xaml.cs`. The service-layer
+// functions in [`crate::services::beanfun::account`] mirror WPF's
+// `BeanfunClient.Account.cs::UnconnectedGame_*` family one-for-one;
+// the command bodies below are zero-logic adapters whose only jobs
+// are:
+//
+// 1. Gate on `require_auth` (every step requires the bfWebToken
+// cookie to be live in the client jar).
+// 2. Pull `service_code` / `service_region` off the active
+// [`Session`][sn] for the steps that need it (init payload +
+// change-password). The frontend never passes these as IPC
+// parameters — same contract as `add_service_account` /
+// `change_display_name` (see those commands' docs for the
+// "single source of truth" rationale).
+// 3. Forward the typed [`AddAccountSession`] / [`AddAccountInit`] /
+// [`CheckOutcome`] / [`AddAccountOutcome`] /
+// [`ChangePasswordOutcome`] DTOs verbatim — all five derive
+// `serde::Serialize` (and [`AddAccountSession`] additionally
+// derives `serde::Deserialize` because the frontend round-trips
+// it through three POSTs as an opaque cursor) + `specta::Type`
+// so `bindings.ts` mirrors the Rust contract.
+//
+// The frontend dialogs (`UnconnectedGame_AddAccount.vue` /
+// `UnconnectedGame_ChangePassword.vue`, P12.3 D6 / D7) own all
+// validation + UX wiring. WPF's pre-flight client-side checks
+// (empty name, length range, password mismatch, contract
+// checkbox) belong on the frontend; the service layer's defensive
+// `LoginError::Unknown(_)` returns for empty inputs are a backstop,
+// not the primary validation surface.
+//
+// [sn]: crate::services::beanfun::Session
+
+/// Open the unconnected-game add-account dialog session — runs the
+/// auth.aspx → 02.aspx GET / POST pair to seed cookies + parse the
+/// initial view-state triplet, returning game name + account-id
+/// length range + nickname-check support flag.
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::unconnected_game_init_add_account_payload`][svc]
+/// verbatim. Pulls `service_code` / `service_region` from the active
+/// [`Session`][sn] (same lock-down as [`add_service_account`] — the
+/// dialog only ever targets the user's currently selected
+/// unconnected game).
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Every [`LoginError`][le] surfaced by the service
+/// (`AccountMgmtMissingViewState` /
+/// `AccountMgmtMissingViewStateGenerator` /
+/// `AccountMgmtMissingEventValidation` /
+/// `AccountMgmtMissingGameName` /
+/// `AccountMgmtMissingAccountLen` from the parser, plus
+/// transport / non-2xx / body-too-large from the HTTP layer).
+///
+/// # Frontend usage
+///
+/// Called once on `UnconnectedGame_AddAccount.vue` mount. The
+/// returned [`AddAccountInit::session`] is stashed in component
+/// state and threaded through every subsequent
+/// [`unconnected_game_add_account_check`] /
+/// [`unconnected_game_add_account_check_nickname`] /
+/// [`unconnected_game_add_account`] call — the frontend treats it
+/// as an opaque cursor (no field inspection).
+///
+/// [svc]: crate::services::beanfun::unconnected_game_init_add_account_payload
+/// [sn]: crate::services::beanfun::Session
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn unconnected_game_init_add_account_payload(
+ state: State<'_, AppState>,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let init = service_unconnected_game_init_add_account_payload(
+ &client,
+ &session,
+ &session.service_code,
+ &session.service_region,
+ )
+ .await?;
+ Ok(init)
+}
+
+/// Validate a candidate account-id (and optional display name)
+/// before final submission — POST `02.aspx` with
+/// `__EVENTTARGET=lbtnCheckAccount`.
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::unconnected_game_add_account_check`][svc]
+/// verbatim:
+///
+/// - `mgmt_session` is the round-tripped [`AddAccountSession`] from
+/// the previous call (`init_add_account_payload` for the first
+/// check, or the prior `CheckOutcome.session` for follow-ups).
+/// - `name` is the candidate account id.
+/// - `account_dn` is the optional display-name field — `Some("")` /
+/// `Some(non_empty)` opt into the `t1` (TW) / `txtServiceAccountDN`
+/// (HK) form field; `None` skips it entirely (matches WPF's
+/// `txtServiceAccountDN != null` gate).
+///
+/// Returns a [`CheckOutcome`] carrying the refreshed view-state
+/// triplet plus the optional `lblErrorMessage` text.
+///
+/// # Errors
+///
+/// As for [`unconnected_game_init_add_account_payload`].
+///
+/// [svc]: crate::services::beanfun::unconnected_game_add_account_check
+#[tauri::command]
+#[specta::specta]
+pub async fn unconnected_game_add_account_check(
+ state: State<'_, AppState>,
+ mgmt_session: AddAccountSession,
+ name: String,
+ account_dn: Option,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let outcome = service_unconnected_game_add_account_check(
+ &client,
+ &session,
+ &mgmt_session,
+ &name,
+ account_dn.as_deref(),
+ )
+ .await?;
+ Ok(outcome)
+}
+
+/// Validate a candidate display name before final submission —
+/// POST `02.aspx` with `__EVENTTARGET=lbtnCheckNickName` (the
+/// account-id field is sent empty for this endpoint).
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::unconnected_game_add_account_check_nickname`][svc]
+/// verbatim. See [`unconnected_game_add_account_check`] for the
+/// `mgmt_session` / `account_dn` round-trip semantics.
+///
+/// # Errors
+///
+/// As for [`unconnected_game_add_account_check`].
+///
+/// [svc]: crate::services::beanfun::unconnected_game_add_account_check_nickname
+#[tauri::command]
+#[specta::specta]
+pub async fn unconnected_game_add_account_check_nickname(
+ state: State<'_, AppState>,
+ mgmt_session: AddAccountSession,
+ account_dn: Option,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let outcome = service_unconnected_game_add_account_check_nickname(
+ &client,
+ &session,
+ &mgmt_session,
+ account_dn.as_deref(),
+ )
+ .await?;
+ Ok(outcome)
+}
+
+/// Finalise unconnected-game account creation — POST `02.aspx`
+/// with the full add-account form (id + password ×2 + optional
+/// display name + `chkBox1=on` + `imgbtn_Submit.x/y=0`).
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::unconnected_game_add_account`][svc]
+/// verbatim. Returns [`AddAccountOutcome::Success`] when the
+/// response carries no (or empty) `lblErrorMessage`, otherwise
+/// [`AddAccountOutcome::ErrorMessage`] carrying the message text.
+///
+/// # Why pre-validate empty inputs?
+///
+/// The service layer rejects empty `name` / `new_password` /
+/// `new_password_confirm` with `LoginError::Unknown(_)` (mapped
+/// to `auth.unknown` at the `CommandError` boundary). This is a
+/// backstop — the frontend dialog (`UnconnectedGame_AddAccount.vue`)
+/// runs WPF-equivalent client-side validation (length range,
+/// password mismatch, contract checkbox) before invoking, so this
+/// path should never trigger in practice. Surfacing the typed
+/// error keeps the contract honest if a future caller bypasses
+/// the dialog.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - `auth.unknown` — empty `name` / `new_password` /
+/// `new_password_confirm` (defensive).
+/// - Any [`LoginError`][le] surfaced by the service.
+///
+/// [svc]: crate::services::beanfun::unconnected_game_add_account
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn unconnected_game_add_account(
+ state: State<'_, AppState>,
+ mgmt_session: AddAccountSession,
+ name: String,
+ new_password: String,
+ new_password_confirm: String,
+ account_dn: Option,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let outcome = service_unconnected_game_add_account(
+ &client,
+ &session,
+ &mgmt_session,
+ &name,
+ &new_password,
+ &new_password_confirm,
+ account_dn.as_deref(),
+ )
+ .await?;
+ Ok(outcome)
+}
+
+/// Drive the 5-step unconnected-game change-password flow — auth
+/// preamble + 01Accounts.aspx GET / POST + 03.aspx GET / POST
+/// (HK uses `http://` for the last 3 steps by upstream design;
+/// see service-layer module docs).
+///
+/// # Contract
+///
+/// Mirrors [`services::beanfun::unconnected_game_change_password`][svc]
+/// verbatim. Returns one of:
+///
+/// - [`ChangePasswordOutcome::VerifyCodeSent`] — server emitted a
+/// `verify_code=` query parameter on the final redirect
+/// URL. Caller surfaces the token to the user so they can paste
+/// it into the Beanfun verify dialog.
+/// - [`ChangePasswordOutcome::ErrorMessage`] — server rendered a
+/// non-empty `lblErrorMessage` span. Caller shows the verbatim
+/// text in the dialog.
+///
+/// # Why pull `service_code` / `service_region` from the session?
+///
+/// Same reason as [`unconnected_game_init_add_account_payload`] —
+/// the dialog only ever targets the user's currently selected
+/// unconnected game.
+///
+/// # Why expose `num` over IPC?
+///
+/// `num` is the 0-based row index inside `gvServiceAccountList`
+/// the user clicked on (WPF `MainWindow.xaml.cs::ResetPassword_Click`
+/// passes `int`; we use `i32` for direct parity). The frontend
+/// gets it from the row position the user invoked "change
+/// password" on, so it has to flow through IPC. The backend has no
+/// other way to know which row the user picked.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - All three `AccountMgmtMissing*` view-state variants from the
+/// parser steps (step 2 + step 4 of the 5-step flow).
+/// - Any [`LoginError`][le] surfaced by the service (transport /
+/// non-2xx on any of the five HTTP calls).
+///
+/// [svc]: crate::services::beanfun::unconnected_game_change_password
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn unconnected_game_change_password(
+ state: State<'_, AppState>,
+ num: i32,
+ email: String,
+) -> Result {
+ let (client, session) = require_auth(state.inner()).await?;
+ let outcome = service_unconnected_game_change_password(
+ &client,
+ &session,
+ &session.service_code,
+ &session.service_region,
+ num,
+ &email,
+ )
+ .await?;
+ Ok(outcome)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::commands::session::SESSION_REQUIRED_CODE;
+ use std::path::PathBuf;
+
+ fn empty_state() -> AppState {
+ AppState::new(PathBuf::from(r"C:\tmp"))
+ }
+
+ /// When [`AppState::auth`] is `None`, the shared helper must
+ /// short-circuit with `auth.session_required`. Asserted on the
+ /// helper (not the commands) so both `get_accounts` and
+ /// `refresh` inherit the behaviour through their one-liner
+ /// delegation — the test is their joint contract.
+ #[tokio::test]
+ async fn list_accounts_internal_without_session_surfaces_session_required() {
+ let app = empty_state();
+ let err = list_accounts_internal(&app)
+ .await
+ .expect_err("no session → error");
+
+ assert_eq!(err.code, SESSION_REQUIRED_CODE);
+ }
+
+ /// All four P10.2 account-family commands must exist with their
+ /// declared signatures. The `_ = ` pattern is a readable
+ /// way to force a symbol reference without invoking the
+ /// `State<'_, _>`-requiring body (Tauri's `State` wrapper can't
+ /// be constructed outside `tauri::test::mock_app()` — which we
+ /// deliberately avoid per the auth-module convention).
+ /// Session-gating is validated through
+ /// [`list_accounts_internal_without_session_surfaces_session_required`]
+ /// and the `require_auth` tests in [`super::super::session`];
+ /// D9 management commands inherit the same behaviour via the
+ /// shared `require_auth` call.
+ #[test]
+ fn account_commands_exist_with_declared_signatures() {
+ let _ = get_accounts;
+ let _ = refresh;
+ let _ = add_service_account;
+ let _ = change_display_name;
+ let _ = get_contract;
+ let _ = get_email;
+ let _ = get_remain_point;
+ let _ = unconnected_game_init_add_account_payload;
+ let _ = unconnected_game_add_account_check;
+ let _ = unconnected_game_add_account_check_nickname;
+ let _ = unconnected_game_add_account;
+ let _ = unconnected_game_change_password;
+ let _ = set_active_service;
+ }
+
+ /// `ServiceAccount` must be **round-trippable** through serde
+ /// so the frontend can echo the object it got from
+ /// [`get_accounts`] back to [`change_display_name`]. Full-field
+ /// equality here guards against a future `#[serde(skip)]` or
+ /// rename slipping in and silently dropping data the service
+ /// layer depends on (`sid` / `sname`) — the rename flow would
+ /// break silently on the transport boundary otherwise.
+ #[test]
+ fn service_account_serde_roundtrip_preserves_all_fields() {
+ let original = sample_service_account();
+ let json = serde_json::to_string(&original).expect("serialize");
+ let decoded: ServiceAccount = serde_json::from_str(&json).expect("deserialize");
+ assert_eq!(decoded, original);
+ }
+
+ fn sample_service_account() -> ServiceAccount {
+ ServiceAccount {
+ is_enable: true,
+ visible: true,
+ is_inherited: false,
+ sid: "sid_test".into(),
+ ssn: "42".into(),
+ sname: "AliceTheFirst".into(),
+ screatetime: Some("2024-01-02 03:04:05".into()),
+ slastusedtime: None,
+ sauthtype: None,
+ }
+ }
+
+ /// `AddAccountSession` must be **round-trippable** through serde
+ /// because the frontend treats it as an opaque cursor — it
+ /// receives the triplet from
+ /// [`unconnected_game_init_add_account_payload`] and re-passes
+ /// it verbatim to [`unconnected_game_add_account_check`] /
+ /// [`unconnected_game_add_account_check_nickname`] /
+ /// [`unconnected_game_add_account`]. A future `#[serde(skip)]`
+ /// or rename slipping into the struct would silently truncate
+ /// the round-trip and the next POST would fail with
+ /// `AccountMgmtMissingViewState` / `…ViewStateGenerator` /
+ /// `…EventValidation` (or, worse, send an empty `region`
+ /// discriminant that throws off the HK `__VIEWSTATEENCRYPTED`
+ /// splice). This assertion is the structural backstop.
+ #[test]
+ fn add_account_session_serde_roundtrip_preserves_all_fields() {
+ let original = AddAccountSession {
+ viewstate: "/wEPDwULLTE4OTQyMTYwOTRkZIIxN…=".into(),
+ viewstate_generator: "B41B0BB6".into(),
+ event_validation: "/wEdAANEYWGV…=".into(),
+ region: crate::services::beanfun::LoginRegion::HK,
+ };
+ let json = serde_json::to_string(&original).expect("serialize");
+ let decoded: AddAccountSession = serde_json::from_str(&json).expect("deserialize");
+ assert_eq!(decoded, original);
+ }
+
+ /// Pin the `tag = "kind", content = "data"` enum representation
+ /// for [`AddAccountOutcome`] — the frontend's discriminated
+ /// union switch (`switch (outcome.kind) { case "success": ...
+ /// case "error_message": ... }`) would silently break if a
+ /// future maintainer dropped the `#[serde(tag = ..., content =
+ /// ...)]` attribute. Matches the equivalent guard
+ /// [`launcher::tests`] applies to [`GameStartMode`].
+ #[test]
+ fn add_account_outcome_serde_shape_is_stable() {
+ let success = serde_json::to_value(AddAccountOutcome::Success).expect("ser");
+ assert_eq!(success, serde_json::json!({"kind": "success"}));
+
+ let err = serde_json::to_value(AddAccountOutcome::ErrorMessage("bad".into())).expect("ser");
+ assert_eq!(
+ err,
+ serde_json::json!({"kind": "error_message", "data": "bad"})
+ );
+ }
+
+ /// As above, for [`ChangePasswordOutcome`] — the frontend
+ /// branches on `outcome.kind` to either show the
+ /// "MsgChangePassword" toast (with `data` as the verify token)
+ /// or render an inline error banner (with `data` as the
+ /// `lblErrorMessage` body).
+ #[test]
+ fn change_password_outcome_serde_shape_is_stable() {
+ let ok = serde_json::to_value(ChangePasswordOutcome::VerifyCodeSent("tok123".into()))
+ .expect("ser");
+ assert_eq!(
+ ok,
+ serde_json::json!({"kind": "verify_code_sent", "data": "tok123"})
+ );
+
+ let err =
+ serde_json::to_value(ChangePasswordOutcome::ErrorMessage("nope".into())).expect("ser");
+ assert_eq!(
+ err,
+ serde_json::json!({"kind": "error_message", "data": "nope"})
+ );
+ }
+
+ // -------------------------------------------------------------
+ // P12.3 D8a — set_active_service tests
+ // -------------------------------------------------------------
+
+ /// Build an [`AuthContext`] seeded with the WPF-default service
+ /// code/region pair so each `set_active_service` test starts
+ /// from the same well-known baseline. Mirrors the helper of the
+ /// same shape in [`super::super::session::tests`] —
+ /// intentionally duplicated rather than re-exported because
+ /// `pub(super)` would force the `session` module to be aware of
+ /// the `account` module's test fixtures (P10.2 cross-module
+ /// test isolation).
+ fn seeded_auth_context() -> crate::commands::state::AuthContext {
+ use crate::services::beanfun::client::{BeanfunClient, ClientConfig, LoginRegion};
+ use crate::services::beanfun::session::Session;
+ let client = BeanfunClient::new(ClientConfig::default()).expect("client builds");
+ let session = Session::new(
+ LoginRegion::TW,
+ "SKEY_TEST",
+ "BFWT_TEST",
+ "alice",
+ "610074",
+ "T9",
+ );
+ crate::commands::state::AuthContext::new(client, session)
+ }
+
+ /// Hot path: switching the active service must mutate
+ /// `session.service_code` / `session.service_region` in place
+ /// so a subsequent `require_auth` snapshot (= the next
+ /// `get_accounts` call) sees the new pair. Asserted by reading
+ /// back through the `RwLock` — exactly the path
+ /// `list_accounts_internal` takes.
+ #[tokio::test]
+ async fn set_active_service_updates_session_pair_in_place() {
+ let app = empty_state();
+ {
+ let mut guard = app.auth.write().await;
+ *guard = Some(seeded_auth_context());
+ }
+
+ set_active_service_internal(&app, "610153".into(), "TN".into())
+ .await
+ .expect("logged-in update should succeed");
+
+ let guard = app.auth.read().await;
+ let ctx = guard.as_ref().expect("auth still populated after swap");
+ assert_eq!(ctx.session.service_code, "610153");
+ assert_eq!(ctx.session.service_region, "TN");
+ }
+
+ /// Other session fields (`account_id`, `region`, secrets) must
+ /// survive a service swap untouched — losing `account_id` would
+ /// break the i18n footer label, losing `region` would point
+ /// every follow-up POST at the wrong host, and losing the
+ /// secrets would force the user to re-login mid-game-switch.
+ /// This is the structural guard that the hot-path test pairs
+ /// with.
+ #[tokio::test]
+ async fn set_active_service_preserves_other_session_fields() {
+ use crate::services::beanfun::client::LoginRegion;
+
+ let app = empty_state();
+ {
+ let mut guard = app.auth.write().await;
+ *guard = Some(seeded_auth_context());
+ }
+
+ set_active_service_internal(&app, "610085".into(), "TC".into())
+ .await
+ .expect("update succeeds");
+
+ let guard = app.auth.read().await;
+ let ctx = guard.as_ref().expect("auth populated");
+ assert_eq!(ctx.session.account_id, "alice");
+ assert_eq!(ctx.session.region, LoginRegion::TW);
+ assert_eq!(ctx.session.skey, "SKEY_TEST");
+ assert_eq!(ctx.session.web_token, "BFWT_TEST");
+ }
+
+ /// `set_active_service` must surface
+ /// [`SESSION_REQUIRED_CODE`][crate::commands::session::SESSION_REQUIRED_CODE]
+ /// when no login is active, mirroring every other
+ /// session-required command. Defends against a future refactor
+ /// that drops the `None` arm — the picker is gated behind the
+ /// auth-required AccountList route, but a non-auth caller
+ /// (background task, unit test, future broker) must still see
+ /// a structured error rather than a silent no-op.
+ #[tokio::test]
+ async fn set_active_service_without_session_surfaces_session_required() {
+ let app = empty_state();
+ let err = set_active_service_internal(&app, "610153".into(), "TN".into())
+ .await
+ .expect_err("no session → error");
+
+ assert_eq!(err.code, SESSION_REQUIRED_CODE);
+ assert!(
+ !err.message.is_empty(),
+ "the message must be non-empty so `tracing` surfaces something useful",
+ );
+ }
+
+ /// Same-game re-selection must be a successful no-op (the
+ /// frontend doesn't gate the picker against picking the
+ /// already-selected game). Asserted to avoid a future
+ /// `if old != new { ... }` micro-optimization that would skip
+ /// the write and break a downstream assumption that the
+ /// command **always** completes the user's intent (including
+ /// any future side-effects we add to it).
+ #[tokio::test]
+ async fn set_active_service_same_pair_is_a_noop_success() {
+ let app = empty_state();
+ {
+ let mut guard = app.auth.write().await;
+ *guard = Some(seeded_auth_context());
+ }
+
+ set_active_service_internal(&app, "610074".into(), "T9".into())
+ .await
+ .expect("re-selecting the current game is fine");
+
+ let guard = app.auth.read().await;
+ let ctx = guard.as_ref().expect("auth populated");
+ assert_eq!(ctx.session.service_code, "610074");
+ assert_eq!(ctx.session.service_region, "T9");
+ }
+
+ /// Empty `(code, region)` strings must be accepted at the
+ /// command boundary — input validation is the service layer's
+ /// responsibility (the next `get_accounts` POST will surface
+ /// the gamezone server-side rejection). This mirrors WPF's
+ /// "the picker dialog supplies whatever it supplies" stance
+ /// (see `MainWindow.GameList.SelectionChanged`) and prevents
+ /// a future overzealous guard from silently swallowing a
+ /// legitimate (but unusual) game code the dialog might surface.
+ #[tokio::test]
+ async fn set_active_service_accepts_empty_strings() {
+ let app = empty_state();
+ {
+ let mut guard = app.auth.write().await;
+ *guard = Some(seeded_auth_context());
+ }
+
+ set_active_service_internal(&app, String::new(), String::new())
+ .await
+ .expect("validation deferred to the next get_accounts call");
+
+ let guard = app.auth.read().await;
+ let ctx = guard.as_ref().expect("auth populated");
+ assert_eq!(ctx.session.service_code, "");
+ assert_eq!(ctx.session.service_region, "");
+ }
+}
diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs
new file mode 100644
index 0000000..7271008
--- /dev/null
+++ b/src-tauri/src/commands/auth.rs
@@ -0,0 +1,2349 @@
+//! Authentication commands — the IPC surface for every login /
+//! logout / OTP interaction the UI can drive.
+//!
+//! # Families exposed in P10.2
+//!
+//! | Command | Family | Purpose |
+//! |----------------------------|----------|---------------------------------------------------------------------------------------------------------------|
+//! | [`login_regular`] | regular | TW / HK username+password single-shot login (handles AdvanceCheck + TOTP detours via `CommandError`) |
+//! | [`login_totp`] | regular | HK two-factor continuation after [`login_regular`] surfaces `auth.totp_required` |
+//! | [`login_qr_start`] | QR | Initialise a QR login session — returns the PNG (Base64 data URL) + optional Beanfun-app deeplink |
+//! | [`login_qr_check`] | QR | Poll the QR handle; on `Approved` the same call finalises the login and sets [`AppState::auth`][st] |
+//! | [`login_gamepass_start`] | gamepass | Mint a fresh `BeanfunClient`, fetch a portal session key, stash both on `pending_gamepass` for the WebView |
+//! | [`get_verify_page_info`] | verify | Fetch the AdvanceCheck verify page (returns the `lblAuthType` label) |
+//! | [`get_verify_captcha`] | verify | Fetch the captcha image (Base64 data URL) |
+//! | [`submit_verify`] | verify | Submit `verify_code + captcha_code`; surfaces `Success` / `WrongCaptcha` / `WrongAuthInfo` / `ServerMessage` |
+//! | [`logout`] | logout | Clear local auth + every pending slot; best-effort server-side `erase_token` (errors logged, never surfaced) |
+//!
+//! [st]: super::state::AppState::auth
+//!
+//! `open_gamepass_window` (P12.1 D5b CP3) and the
+//! `login_gamepass_complete` follow-up are deliberately split out of
+//! D5a so the backend changes here can land + run quality gates
+//! independently of the [`tauri::WebviewWindow`] cookie-extraction
+//! plumbing.
+//!
+//! # Continuation state machine
+//!
+//! The regular family is a **two-step** interaction for the HK /
+//! MapleStory TOTP path — otherwise it's single-shot. The backend
+//! retains continuation state across the two IPC round-trips so the
+//! frontend never holds server-side secrets.
+//!
+//! ```text
+//! frontend (Vue) backend (this module)
+//! ────────────── ─────────────────────
+//! invoke('login_regular', {region,account,password})
+//! │ │
+//! │ ┌───────────────────────────────────────┐ │
+//! │ │ login_with(..) → Ok(session) │◀──┘
+//! │ └───────────────────────────────────────┘ │ happy path
+//! ◀─────────────── SessionInfo ─────────────────┘
+//! │
+//! ---------- or the HK-TOTP detour ----------
+//! │
+//! │ ┌───────────────────────────────────────┐
+//! │ │ login_with(..) → TotpRequired(ch) │
+//! │ │ pending_totp = Some((client, ch)) │
+//! │ └───────────────────────────────────────┘
+//! ◀── CommandError │
+//! { code: 'auth.totp_required', │
+//! details: TotpChallengeInfo } │
+//! │ (frontend renders 6-digit OTP prompt) │
+//! invoke('login_totp', { code: '123456' }) │
+//! │ │
+//! │ ┌───────────────────────────────────────┐ │
+//! │ │ pending_totp.read().clone() │ │
+//! │ │ login_totp_service(..) │ │
+//! │ │ Ok(session) → clear pending, │ │
+//! │ │ set auth │ │
+//! │ │ Err(..) → keep pending slot │ │
+//! │ │ for user retry │ │
+//! │ └───────────────────────────────────────┘ │
+//! ◀─────────────── SessionInfo ──────────────────┘
+//! ```
+//!
+//! # Why clone out of [`AppState::pending_totp`] instead of `take`?
+//!
+//! Calling [`Option::take`] on the write guard is simpler but loses
+//! the WPF retry UX: on a wrong OTP the server just shows "wrong
+//! code" and the user types again — the challenge / login session
+//! cookies are still valid. A blanket `take` would force the user
+//! back to re-entering username+password on every mistyped digit.
+//! Cloning keeps the slot populated until a
+//! [`services::beanfun::login::login_totp`][crate::services::beanfun::login::login_totp]
+//! call resolves to `Ok(_)`, at which point the login pipeline
+//! succeeded and the continuation is no longer needed.
+//!
+//! Cancellation (user hits "Cancel" on the OTP prompt) is handled
+//! by the D7 `logout` command, which clears both `auth` and
+//! `pending_totp` in one swoop. P10.2 intentionally does not expose
+//! a separate `cancel_totp` — YAGNI until the Vue UX (P11/P12) has
+//! a concrete screen that would benefit from the narrower cmd.
+//!
+//! [`AppState::pending_totp`]: super::state::AppState::pending_totp
+
+use serde::Serialize;
+use specta::Type;
+use tauri::{
+ webview::PageLoadEvent, AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindow,
+ WebviewWindowBuilder, WindowEvent,
+};
+use url::Url;
+
+use crate::commands::{
+ dto::{encode_png_base64, SessionInfo, TotpChallengeInfo},
+ error::CommandError,
+ state::{AppState, AuthContext, PendingGamepass, PendingQr, PendingTotp, PendingVerify},
+};
+use crate::services::beanfun::{
+ client::{BeanfunClient, ClientConfig, LoginRegion},
+ login::{
+ finalize_qr_login, get_session_key, init_qr_login, inject_webview_cookies,
+ login_totp as login_totp_service, login_with, logout as logout_service,
+ poll_qr_login_status, seed_webview_cookies_from_client, try_complete_gamepass_login,
+ LoginMethod, QrPollOutcome,
+ },
+ session::Credentials,
+ verify::{
+ get_verify_captcha as get_verify_captcha_service,
+ get_verify_page_info as get_verify_page_info_service,
+ submit_verify as submit_verify_service, VerifyOutcome,
+ },
+ LoginError,
+};
+
+/// Error code surfaced to the frontend when [`login_totp`] runs and
+/// there is no pending TOTP challenge on [`AppState::pending_totp`].
+///
+/// Exposed as a `pub(crate)` const so tests can assert against the
+/// exact wire string without a second source of truth.
+pub(crate) const TOTP_NOT_PENDING_CODE: &str = "auth.totp_not_pending";
+
+/// Error code surfaced when [`login_totp`] is called with a `code`
+/// that is not exactly 6 ASCII digits. Defensive — the Vue form
+/// should validate up front, but a hostile caller could bypass the
+/// UI and invoke the command directly.
+pub(crate) const TOTP_INVALID_CODE: &str = "auth.totp_invalid_code";
+
+/// TOTP digit count — matches WPF's six `otpCode1..6` form fields
+/// and [`crate::services::beanfun::login::login_totp`]'s six `&str`
+/// parameters.
+const TOTP_DIGITS: usize = 6;
+
+/// Split a user-typed OTP string into the six individual ASCII
+/// digits that
+/// [`crate::services::beanfun::login::login_totp`][super::super::services::beanfun::login::login_totp]
+/// expects, and surface a clean [`CommandError`] on malformed input.
+///
+/// The service layer takes six `&str` arguments (to mirror WPF's
+/// `otpCode1..6` fields 1:1 for cross-reference), but the IPC
+/// boundary is cleaner with a single `code: String` — the Vue form
+/// concatenates six digit boxes into one value anyway. This helper
+/// bridges the two shapes and validates the input.
+///
+/// # Validation
+///
+/// Accepts exactly 6 ASCII digits (`0..=9`). A `code` that is:
+/// - shorter or longer than 6 characters
+/// - contains any non-digit (including full-width digits, spaces,
+/// letters)
+///
+/// surfaces `auth.totp_invalid_code` without reaching the HTTP POST.
+/// This keeps the WPF behaviour (which would simply fail server-side
+/// with a generic error) but fails faster and with a localisable
+/// error code the UI can special-case.
+fn split_otp_digits(code: &str) -> Result<[String; TOTP_DIGITS], CommandError> {
+ let chars: Vec = code.chars().collect();
+ if chars.len() != TOTP_DIGITS || !chars.iter().all(|c| c.is_ascii_digit()) {
+ return Err(CommandError::new(
+ TOTP_INVALID_CODE,
+ format!("TOTP code must be exactly {TOTP_DIGITS} ASCII digits."),
+ ));
+ }
+ Ok([
+ chars[0].to_string(),
+ chars[1].to_string(),
+ chars[2].to_string(),
+ chars[3].to_string(),
+ chars[4].to_string(),
+ chars[5].to_string(),
+ ])
+}
+
+/// Classify a [`LoginRegion`] into a [`LoginMethod`] bound to the
+/// region's default service code + region.
+///
+/// P10.2 pins the service code / region to
+/// [`LoginRegion::default_service_code`] /
+/// [`LoginRegion::default_service_region`] (MapleStory — the same
+/// defaults WPF shipped with). Once the Vue UI lands a game picker
+/// (P11/P12), the HK arm will gain optional parameters threaded
+/// through here.
+fn default_method_for(region: LoginRegion) -> LoginMethod<'static> {
+ match region {
+ LoginRegion::TW => LoginMethod::TwRegular,
+ LoginRegion::HK => LoginMethod::HkRegular {
+ service_code: region.default_service_code(),
+ service_region: region.default_service_region(),
+ },
+ }
+}
+
+/// TW / HK regular username+password login.
+///
+/// # Protocol
+///
+/// 1. Best-effort clear [`AppState::pending_totp`]
+/// ([`AppState`]) so a stale continuation from an abandoned
+/// HK-TOTP attempt cannot leak into the new login's error
+/// surface.
+/// 2. Mint a fresh [`BeanfunClient`] with region-appropriate
+/// endpoints.
+/// 3. Run [`login_with`] through the regular-family dispatcher.
+/// 4. On success: stash `(client, session)` on [`AppState::auth`] and
+/// return a [`SessionInfo`] DTO to the frontend.
+/// 5. On [`LoginError::TotpRequired`]: stash `(client, challenge)`
+/// on [`AppState::pending_totp`] and surface
+/// `auth.totp_required` with a [`TotpChallengeInfo`] details
+/// payload. The Vue layer is expected to render an OTP prompt
+/// and call [`login_totp`] with the result.
+/// 6. On every other [`LoginError`] variant: delegate to the P10.1
+/// [`From`][`CommandError`] impl — including
+/// [`LoginError::AdvanceCheckRequired`] which surfaces
+/// `auth.advance_check_required` with the challenge URL for the
+/// frontend to drive a verify flow.
+///
+/// # Why take `account` + `password` by value?
+///
+/// `#[tauri::command]` deserialises arguments from the JS invoke
+/// payload into owned `String`s anyway; borrowing would force an
+/// extra lifetime parameter that `specta` cannot round-trip. The
+/// owned `String` is immediately wrapped in [`Credentials`] whose
+/// [`Drop`] implementation zeroises the password byte buffer (via
+/// `zeroize::ZeroizeOnDrop`), so the plaintext's lifetime is bounded
+/// by the body of this function.
+///
+/// # Why mint a fresh client per call?
+///
+/// [`BeanfunClient`] owns the cookie jar. A re-login must begin with
+/// a clean jar so stale `_SESSIONID` / `BFCOOKIE` cookies from the
+/// previous attempt don't collide with the new one; WPF achieves the
+/// same guarantee by instantiating a new `HttpClient` on every login
+/// dialog open (Login.cs L38-41).
+#[tauri::command]
+#[specta::specta]
+pub async fn login_regular(
+ state: State<'_, AppState>,
+ region: LoginRegion,
+ account: String,
+ password: String,
+) -> Result {
+ *state.pending_totp.write().await = None;
+ *state.pending_qr.write().await = None;
+
+ let client = BeanfunClient::new(ClientConfig::for_region(region))?;
+ let creds = Credentials::new(account, password);
+ let method = default_method_for(region);
+
+ let outcome = login_with(&client, method, &creds).await;
+
+ drop(creds);
+
+ match outcome {
+ Ok(session) => {
+ let info = SessionInfo::from(&session);
+ *state.auth.write().await = Some(AuthContext::new(client, session));
+ Ok(info)
+ }
+ Err(LoginError::TotpRequired(challenge)) => {
+ let display = TotpChallengeInfo::from(&*challenge);
+ *state.pending_totp.write().await = Some(PendingTotp::new(client, *challenge));
+ Err(CommandError::new(
+ "auth.totp_required",
+ "TOTP one-time password required to complete login.",
+ )
+ .with_details(&display))
+ }
+ Err(err) => Err(err.into()),
+ }
+}
+
+/// Complete an HK TOTP login by submitting the 6-digit code stored
+/// on [`AppState::pending_totp`].
+///
+/// # Preconditions
+///
+/// Must be preceded by a [`login_regular`] call that resolved with
+/// `auth.totp_required`. Otherwise surfaces [`TOTP_NOT_PENDING_CODE`].
+///
+/// # Behaviour on error
+///
+/// The pending slot is **retained** on error so the user can retry
+/// with a corrected code (wrong OTP, transient network hiccup). It
+/// is cleared only when:
+///
+/// - the call resolves with `Ok(session)` (the server accepted the
+/// code, the challenge is consumed by design), or
+/// - the user explicitly cancels via the `logout` command (D7).
+///
+/// See the module docs for the full state machine.
+///
+/// # Why `code` is a single `String` (not six)?
+///
+/// The IPC shape matches what the Vue form builds (`"123456"`);
+/// splitting happens in [`split_otp_digits`] right before the
+/// service call. The service layer's six-param signature mirrors
+/// WPF's `otpCode1..6` 1:1 — we honour that at the call site
+/// without forcing every TypeScript caller to destructure into six
+/// boxes.
+#[tauri::command]
+#[specta::specta]
+pub async fn login_totp(
+ state: State<'_, AppState>,
+ code: String,
+) -> Result {
+ let digits = split_otp_digits(&code)?;
+
+ let (client, challenge) = {
+ let guard = state.pending_totp.read().await;
+ let pt = guard.as_ref().ok_or_else(|| {
+ CommandError::new(
+ TOTP_NOT_PENDING_CODE,
+ "No TOTP challenge is pending; please log in again.",
+ )
+ })?;
+ (pt.client.clone(), pt.challenge.clone())
+ };
+
+ let session = login_totp_service(
+ &client, &challenge, &digits[0], &digits[1], &digits[2], &digits[3], &digits[4], &digits[5],
+ )
+ .await?;
+
+ *state.pending_totp.write().await = None;
+ let info = SessionInfo::from(&session);
+ *state.auth.write().await = Some(AuthContext::new(client, session));
+ Ok(info)
+}
+
+// ═══════════════════════════════════════════════════════════════════════
+// QR family
+// ═══════════════════════════════════════════════════════════════════════
+
+/// Error code surfaced by [`login_qr_check`] when no QR login is
+/// active on [`AppState::pending_qr`].
+pub(crate) const QR_NOT_STARTED_CODE: &str = "auth.qr_not_started";
+
+/// The safe-subset DTO returned by [`login_qr_start`] — everything
+/// the frontend needs to render a QR scanner UI, and nothing more.
+///
+/// # What's inside
+///
+/// - `bitmap_base64` — the full `data:image/png;base64,<…>` data
+/// URL. Drops straight into an `
`.
+/// - `deeplink` — optional Beanfun-app deeplink the user can tap on
+/// mobile instead of scanning.
+///
+/// # What's **NOT** inside
+///
+/// - `skey` (portal session key) — a backend-only secret.
+/// - `verification_token` (antiforgery token) — also backend-only;
+/// [`login_qr_check`] replays it from [`PendingQr`] directly.
+///
+/// Keeping both secrets backend-side means a hostile (or buggy)
+/// frontend cannot forge poll / finalize requests bypassing the
+/// command handlers. Mirrors the [`TotpChallenge`][tc] →
+/// [`TotpChallengeInfo`] split.
+///
+/// [tc]: crate::services::beanfun::login::TotpChallenge
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+pub struct QrStart {
+ /// `data:image/png;base64,...` data URL — preserves WPF's exact
+ /// storage shape (`bitmapBase64 = "data:image/png;base64," +
+ /// base64`, `BeanfunClient.Login.cs` L449).
+ pub bitmap_base64: String,
+ /// Normalised Beanfun-app deeplink, or `None` if the server did
+ /// not provide one.
+ pub deeplink: Option,
+}
+
+/// Poll result for [`login_qr_check`].
+///
+/// Internally-tagged serde enum — JSON shapes:
+///
+/// ```json
+/// { "status": "pending" }
+/// { "status": "retry" }
+/// { "status": "expired" }
+/// { "status": "approved", "session": {...SessionInfo...} }
+/// ```
+///
+/// The Vue poll loop is expected to pattern-match on `status`:
+///
+/// - `pending` — user has not yet confirmed in the mobile app;
+/// keep polling on the next tick.
+/// - `retry` — server reported a round-trip failure but the
+/// challenge is still live; keep polling. Mirrors WPF's
+/// `ResultMessage == "Failed"` branch (which kept the timer
+/// running).
+/// - `expired` — QR token aged out; the backend has already
+/// cleared [`PendingQr`]. Frontend should call [`login_qr_start`]
+/// again to refresh the QR (WPF UI does the same at
+/// `MainWindow.qrCheckLogin_Tick` L2364-2367 →
+/// `refreshQRCode()`).
+/// - `approved` — user confirmed the scan in the mobile app;
+/// `login_qr_check` internally ran
+/// [`finalize_qr_login`] + set [`AppState::auth`], so the returned
+/// `session` is already live.
+///
+/// [`finalize_qr_login`]: crate::services::beanfun::login::finalize_qr_login
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+#[serde(tag = "status", rename_all = "snake_case")]
+pub enum QrStatus {
+ /// `ResultMessage == "Wait Login"` — user hasn't scanned yet.
+ Pending,
+ /// `ResultMessage == "Failed"` — transient round-trip failure;
+ /// keep polling.
+ Retry,
+ /// `ResultMessage == "Token Expired"` — challenge consumed;
+ /// backend has already cleared the pending slot.
+ Expired,
+ /// `ResultMessage == "Success"` — scan confirmed; the backend
+ /// finalised the login and the session is now live.
+ Approved {
+ /// The freshly-minted session, post-finalize.
+ session: SessionInfo,
+ },
+}
+
+/// Begin a QR-code login flow — fetch the QR PNG, park the
+/// continuation on [`AppState::pending_qr`], and return the
+/// display payload.
+///
+/// # Preconditions
+///
+/// None. Calling this command repeatedly is the "refresh QR"
+/// operation — each call mints a fresh [`BeanfunClient`] (clean
+/// cookie jar) and overwrites any prior pending QR. Mirrors WPF
+/// `MainWindow.xaml.cs::refreshQRCode()` which re-runs the whole
+/// init sequence.
+///
+/// # Side effects
+///
+/// - Clears any prior `pending_totp` (switching login method
+/// invalidates any half-finished TOTP continuation).
+/// - Clears any prior `pending_qr` (explicit refresh semantics).
+/// - Populates `pending_qr = Some((client, init))` on success so
+/// [`login_qr_check`] can drive the poll / finalize cycle.
+///
+/// # Region restriction
+///
+/// QR login is **TW-only** — HK portal does not expose the same
+/// `Login/InitLogin` endpoint (WPF disables the QR button under
+/// `MainWindow.xaml.cs::loginMethodInit` L1099-1114). The region
+/// parameter is kept for symmetry with [`login_regular`], but a
+/// non-TW value bubbles up [`LoginError::QrUnsupportedRegion`]
+/// (surfaces as `auth.qr_unsupported_region`).
+#[tauri::command]
+#[specta::specta]
+pub async fn login_qr_start(
+ state: State<'_, AppState>,
+ region: LoginRegion,
+) -> Result {
+ *state.pending_totp.write().await = None;
+ *state.pending_qr.write().await = None;
+
+ let client = BeanfunClient::new(ClientConfig::for_region(region))?;
+ let skey = get_session_key(&client).await?;
+ let init = init_qr_login(&client, &skey).await?;
+
+ let start = QrStart {
+ bitmap_base64: init.bitmap_base64.clone(),
+ deeplink: init.deeplink.clone(),
+ };
+
+ *state.pending_qr.write().await = Some(PendingQr::new(client, init));
+ Ok(start)
+}
+
+/// Poll an active QR login for status — and on success, finalise
+/// the login internally so the frontend gets a ready-to-use
+/// [`SessionInfo`] in one round-trip.
+///
+/// # Preconditions
+///
+/// Must be preceded by a successful [`login_qr_start`]. Otherwise
+/// surfaces [`QR_NOT_STARTED_CODE`] (`auth.qr_not_started`).
+///
+/// # State transitions
+///
+/// - [`QrPollOutcome::WaitLogin`] / [`QrPollOutcome::Failed`] —
+/// pending slot kept; return `Pending` / `Retry`.
+/// - [`QrPollOutcome::TokenExpired`] — pending slot cleared (the
+/// challenge is consumed); return `Expired`. Frontend must call
+/// [`login_qr_start`] again.
+/// - [`QrPollOutcome::Approved`] — run
+/// [`finalize_qr_login`][fin] with the same client, clear the
+/// pending slot, populate [`AppState::auth`], and return
+/// `Approved { session }`.
+///
+/// # Why finalize inline?
+///
+/// P10.2 Q5 = B: split the frontend-visible flow into two commands
+/// (`start` + `check`) so the poll loop is frontend-driven, but
+/// keep the terminal `finalize` step backend-internal so the
+/// session secrets (`web_token`, `skey`) never cross IPC. A
+/// hypothetical third `login_qr_finalize` command would either
+/// duplicate this internal call or leak the init payload to the
+/// frontend — neither aligns with the DRY / no-secrets
+/// contracts.
+///
+/// [fin]: crate::services::beanfun::login::finalize_qr_login
+#[tauri::command]
+#[specta::specta]
+pub async fn login_qr_check(state: State<'_, AppState>) -> Result {
+ let (client, init) = {
+ let guard = state.pending_qr.read().await;
+ let pq = guard.as_ref().ok_or_else(|| {
+ CommandError::new(
+ QR_NOT_STARTED_CODE,
+ "No QR login is active; call login_qr_start first.",
+ )
+ })?;
+ (pq.client.clone(), pq.init.clone())
+ };
+
+ let outcome = poll_qr_login_status(&client, &init).await?;
+
+ match outcome {
+ QrPollOutcome::WaitLogin => Ok(QrStatus::Pending),
+ QrPollOutcome::Failed => Ok(QrStatus::Retry),
+ QrPollOutcome::TokenExpired => {
+ *state.pending_qr.write().await = None;
+ Ok(QrStatus::Expired)
+ }
+ QrPollOutcome::Approved => {
+ let session = finalize_qr_login(&client, &init).await?;
+ *state.pending_qr.write().await = None;
+ let info = SessionInfo::from(&session);
+ *state.auth.write().await = Some(AuthContext::new(client, session));
+ Ok(QrStatus::Approved { session: info })
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════
+// GamePass family
+// ═══════════════════════════════════════════════════════════════════════
+
+/// Error code surfaced by [`open_gamepass_window`] when no GamePass
+/// login is active on [`AppState::pending_gamepass`] at the moment
+/// of invocation — same wire-string shape as
+/// [`QR_NOT_STARTED_CODE`] / [`TOTP_NOT_PENDING_CODE`] /
+/// [`VERIFY_NOT_STARTED_CODE`] so the Vue error router handles it
+/// uniformly.
+///
+/// Scope is **only** the empty-`pending_gamepass` precondition.
+/// The "already-open window" precondition has its own dedicated
+/// code [`GAMEPASS_WINDOW_ALREADY_OPEN_CODE`] so operator logs and
+/// the Vue error pipeline can attribute the two failure modes
+/// distinctly (CP4 debt fix — earlier the two branches shared this
+/// constant which made the "not_started" log line lie when the
+/// real cause was a stale window).
+pub(crate) const GAMEPASS_NOT_STARTED_CODE: &str = "auth.gamepass_not_started";
+
+/// Error code surfaced by [`open_gamepass_window`] when a prior
+/// GamePass WebView window labelled [`GAMEPASS_WINDOW_LABEL`] is
+/// still alive at the moment of invocation. Distinct from
+/// [`GAMEPASS_NOT_STARTED_CODE`] because the underlying remediation
+/// is different — the user must close the existing window before
+/// retrying, not call `login_gamepass_start` again.
+///
+/// The frontend renders the same `windowError` banner for both
+/// codes (UX is uniform: "press Refresh to retry"), but the
+/// localised toast text and the operator log attribution diverge
+/// so postmortems can pinpoint the real cause.
+pub(crate) const GAMEPASS_WINDOW_ALREADY_OPEN_CODE: &str = "auth.gamepass_window_already_open";
+
+/// Fixed Tauri window label for the GamePass WebView. Deliberately
+/// singular: a second invocation of [`open_gamepass_window`] while
+/// the prior window is still alive returns
+/// [`GAMEPASS_WINDOW_ALREADY_OPEN_CODE`] rather than spawning a
+/// duplicate window. Matches WPF
+/// `gamepass_form.xaml.cs::btn_OpenGamePass_Click` (L37-59) which
+/// always allocates exactly one `GamePassBrowser` instance per
+/// login attempt.
+const GAMEPASS_WINDOW_LABEL: &str = "gamepass-login";
+
+/// Tauri event names emitted by the GamePass flow. Flat dash-case
+/// per the P12.1 D5 event convention.
+///
+/// # Emission rules
+///
+/// - [`GAMEPASS_SUCCESS_EVENT`]: emitted exactly once when
+/// [`try_complete_gamepass_login`] resolves to `Some(session)`,
+/// immediately before the window is closed. Payload:
+/// [`SessionInfo`] (the same safe-subset DTO login commands
+/// return).
+/// - [`GAMEPASS_FAILED_EVENT`]: emitted when the page-load cookie
+/// harvest fails for every harvest URL on a given page-load tick
+/// (Tauri runtime error; matches the WPF `ErrorMessage`
+/// branch in `TryCompleteLogin` L158-162). Payload:
+/// [`CommandError`].
+/// - [`GAMEPASS_CANCELLED_EVENT`]: emitted when the window is
+/// destroyed **without** a prior success. Distinguished from
+/// success by the `pending_gamepass` slot — success clears it to
+/// `None` before closing, user-cancel leaves it `Some(_)`.
+/// Payload: none (`()`).
+const GAMEPASS_SUCCESS_EVENT: &str = "gamepass-login-success";
+const GAMEPASS_FAILED_EVENT: &str = "gamepass-login-failed";
+const GAMEPASS_CANCELLED_EVENT: &str = "gamepass-login-cancelled";
+
+/// HTTPS origins the WebView must be polled for during a GamePass
+/// completion check. Mirrors WPF `GamePassBrowser.TryCompleteLogin`
+/// L123-138, which calls `CoreWebView2.CookieManager.GetCookiesAsync`
+/// once per each of these three hosts and merges the results.
+///
+/// Order is not observable (completion depends only on whether
+/// `bfWebToken` is visible on the portal origin after all inserts
+/// land), but we keep it stable and portal-first so traces read
+/// "happy path first" in operator logs.
+const GAMEPASS_HARVEST_URLS: &[&str] = &[
+ "https://tw.beanfun.com",
+ "https://login.beanfun.com",
+ "https://tw.newlogin.beanfun.com",
+];
+
+/// URL path markers WPF's `GamePassBrowser.OnNavigationCompleted`
+/// uses as "redirect has landed on beanfun.com, try completion now"
+/// signals. Every other URL (Login/Index entry page, intermediate
+/// OAuth hops on `gamepass.beanfun.com`, captcha iframes, etc.) is
+/// a no-op for completion — we wait for the next page load.
+///
+/// Matching by substring (not exact path) keeps us compatible with
+/// minor URL shape tweaks on the portal side (e.g. query-string
+/// additions) and mirrors the WPF `uri.Contains("return.aspx")`
+/// check verbatim.
+const GAMEPASS_COMPLETION_PATH_MARKERS: &[&str] = &["return.aspx", "index.aspx", "SendLogin"];
+
+/// JavaScript injected via Tauri's `initialization_script` that
+/// auto-clicks the GamePass login button once the entry page's
+/// DOM is ready.
+///
+/// # Why `initialization_script` (not `eval` in `on_page_load`)?
+///
+/// The WPF reference calls `webView.ExecuteScriptAsync("...click()")`
+/// inside the `NavigationCompleted` handler (`GamePassBrowser.xaml.cs`
+/// L78-90), which races against the in-page script that renders
+/// the `.use-gama-pass` anchor. In Tauri 2, `initialization_script`
+/// runs **before** any page script, so we register a
+/// `DOMContentLoaded` listener inside the script itself and the
+/// click fires reliably after the anchor exists.
+///
+/// # Idempotence & scope-narrowing
+///
+/// The script is injected into **every** page the WebView loads
+/// (Tauri has no URL-filter for init scripts), so we must make it
+/// safe on pages that don't have a GamePass button. The
+/// `querySelector` returns `null` on those pages and the
+/// conditional keeps us silent. The outer IIFE + no globals keeps
+/// the injection from leaking names into the portal's own JS.
+const GAMEPASS_AUTOCLICK_JS: &str = r#"(() => {
+ const clickButton = () => {
+ const anchor = document.querySelector("a.use-gama-pass");
+ if (anchor) {
+ anchor.click();
+ }
+ };
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", clickButton, { once: true });
+ } else {
+ clickButton();
+ }
+})();"#;
+
+/// Strip query parameters from a URL for safe logging.
+/// Prevents session tokens (e.g. `pSKey`) from leaking into traces.
+fn redact_url_query(url: &Url) -> String {
+ let mut redacted = url.clone();
+ redacted.set_query(None);
+ if url.query().is_some() {
+ format!("{}?[REDACTED]", redacted)
+ } else {
+ redacted.to_string()
+ }
+}
+
+/// Check whether `url` is a landing page WPF's
+/// `GamePassBrowser.OnNavigationCompleted` would have triggered
+/// completion for.
+///
+/// The filter is deliberately identical to WPF's — host ends with
+/// `beanfun.com` **and** path carries one of the three completion
+/// markers. Broadening it would cost nothing functionally
+/// ([`try_complete_gamepass_login`] returns `None` when the token
+/// isn't ready yet), but pinning the WPF semantics makes behavioural
+/// regressions in the completion sequence easy to attribute to
+/// either "upstream portal URL changed" or "we deviated from WPF".
+fn should_try_gamepass_completion(url: &Url) -> bool {
+ let Some(host) = url.host_str() else {
+ return false;
+ };
+ if !host.ends_with("beanfun.com") {
+ return false;
+ }
+ let path = url.path();
+ GAMEPASS_COMPLETION_PATH_MARKERS
+ .iter()
+ .any(|marker| path.contains(marker))
+}
+
+/// Parse a harvest origin constant into a [`Url`] — infallible by
+/// construction because the constants are static HTTPS origins, but
+/// factored out so the assertion reads at every call site.
+fn parse_harvest_url(raw: &str) -> Url {
+ Url::parse(raw).expect("GAMEPASS_HARVEST_URLS entry must be a valid absolute URL")
+}
+
+/// Diagnostic — dump every unexpired cookie in `client`'s jar to the
+/// tracing pipeline as structured `info!` records, one per cookie
+/// plus a summary line.
+///
+/// # Why this exists
+///
+/// Live test 2026-04-18 surfaced a failure mode where the GamePass
+/// WebView received a cookie seed (`seeded=2 failed=0` in the
+/// existing [`super::auth::open_gamepass_window`] summary log) but
+/// beanfun's `return.aspx` still rejected the OAuth round-trip with
+/// `Get SecretCode Success(…) but get data fail: (0) No such auth
+/// key and secret code.` — meaning the *wrong* set of session
+/// cookies got seeded, not that seeding itself failed. The CP3-era
+/// summary log only counts cookies, which can't distinguish
+/// "two host-only cookies on `tw.newlogin.beanfun.com`" (the WPF
+/// reference's `CookieContainer.GetCookies(tw.beanfun.com)` filter
+/// would have dropped these) from "two parent-domain cookies on
+/// `.beanfun.com`" (the shape WPF actually seeds).
+///
+/// This helper logs per-cookie attributes — **including the
+/// `CookieDomain` enum** from `cookie_store` which is the exact
+/// distinction we need for the host-only vs subdomain-match
+/// triage — so a live test trace is enough to tell the two
+/// failure modes apart without a debugger attach.
+///
+/// # Call sites
+///
+/// Invoked twice on the GamePass flow for cross-verification:
+///
+/// 1. [`login_gamepass_start`] — right after `get_session_key`
+/// returns. Captures what the portal's redirect chain left
+/// behind in the client jar (the "what WPF's `bfClient` would
+/// be holding at this point" snapshot).
+/// 2. [`open_gamepass_window`] — right before
+/// [`seed_webview_cookies_from_client`] runs. Captures what we
+/// actually hand off to the WebView.
+///
+/// Both dumps should be identical in the happy path (no HTTP
+/// happens between them), but pinning both makes any unexpected
+/// state change (cookie expiry, jar corruption) obvious and
+/// localises the blame.
+///
+/// # Logged fields
+///
+/// Per cookie: `name`, `domain` (the `CookieDomain` enum), `path`
+/// (the `CookiePath` enum), `secure`, `http_only`, `same_site`.
+/// The raw cookie `value` is **intentionally not** logged — session
+/// cookies are credentials-equivalent and structured log sinks
+/// (file, Tauri console, Sentry breadcrumbs) would capture them.
+///
+/// Summary: total unexpired cookie count.
+fn trace_cookie_jar(step: &'static str, client: &BeanfunClient) {
+ let store = client.cookie_store();
+ let guard = match store.lock() {
+ Ok(g) => g,
+ Err(err) => {
+ tracing::warn!(
+ step = step,
+ error = ?err,
+ "cookie store mutex poisoned; skipping jar dump"
+ );
+ return;
+ }
+ };
+
+ let mut count = 0usize;
+ for cookie in guard.iter_unexpired() {
+ // `cookie.domain` / `cookie.path` are the `cookie_store::Cookie`
+ // struct fields (CookieDomain / CookiePath enums) — NOT the
+ // `RawCookie::domain()` / `RawCookie::path()` methods reached
+ // through Deref. The enum form is what this dump exists for:
+ // it distinguishes `HostOnly(host)` (no `Domain` attribute on
+ // the `Set-Cookie`; pinned to the request host) from
+ // `Suffix(host)` (explicit `Domain` attribute; matches host +
+ // subdomains), which is exactly the discrimination that
+ // surfaced the 2026-04-18 seed-fail regression. The method
+ // form would have collapsed both to `Option<&str>` — `None`
+ // for host-only — and hidden the distinction.
+ tracing::info!(
+ step = step,
+ name = cookie.name(),
+ domain = ?cookie.domain,
+ path = ?cookie.path,
+ secure = cookie.secure().unwrap_or(false),
+ http_only = cookie.http_only().unwrap_or(false),
+ same_site = ?cookie.same_site(),
+ "cookie jar entry"
+ );
+ count += 1;
+ }
+
+ tracing::info!(step = step, total = count, "cookie jar dump complete");
+}
+
+/// Tauri event-driven worker that runs on every WebView page-load
+/// completion.
+///
+/// # Flow (mirrors WPF `GamePassBrowser.TryCompleteLogin` L119-163)
+///
+/// 1. Snapshot `(client, skey)` from [`AppState::pending_gamepass`].
+/// If the slot is already `None`, the flow was cancelled /
+/// completed on a prior tick — bail out silently (same stance as
+/// WPF's "early return without error" in L143-144).
+/// 2. URL-filter the tick: only "return.aspx / index.aspx /
+/// SendLogin" landings warrant a completion attempt. Every other
+/// page-load (Login/Index entry, OAuth intermediaries) is a
+/// no-op.
+/// 3. Per [`GAMEPASS_HARVEST_URLS`] origin, pull the WebView's
+/// `cookies_for_url` view and feed it into the client's cookie
+/// jar via [`inject_webview_cookies`]. One call per origin so
+/// `cookie_store`'s RFC 6265 domain-match check runs against the
+/// correct reference URL — merging the three sets under one
+/// origin would silently misclassify cookies whose `Domain`
+/// attribute doesn't match the merged origin.
+/// 4. Call [`try_complete_gamepass_login`]:
+/// - `None` → token not visible from the portal origin yet;
+/// leave `pending_gamepass` populated so the next page-load
+/// tick retries (WPF L143-144).
+/// - `Some(session)` → CAS the pending slot (`take()`); only
+/// the first taker wins, later ticks see `None` and bail.
+/// Winner populates [`AppState::auth`], emits
+/// [`GAMEPASS_SUCCESS_EVENT`], closes the WebView window.
+///
+/// # Why spawn onto the async runtime?
+///
+/// Tauri's `cookies_for_url` is a sync call whose backing IPC can
+/// **deadlock the WebView2 dispatcher** on Windows when invoked
+/// from a synchronous event handler (the on_page_load closure runs
+/// on the WebView2 message-pump thread). Spawning the work onto
+/// `tauri::async_runtime::spawn` bounces it to a tokio worker, out
+/// of the danger zone. Documented upstream: see wry#583 — and the
+/// `tauri::WebviewWindow::cookies`/`cookies_for_url` doc comments
+/// explicitly recommend this pattern.
+///
+/// # Tracing schema (for live-test fault isolation)
+///
+/// Every branch emits a structured `tracing::info!` with a
+/// `step = "Gamepass*"` tag so operators can follow a single page
+/// load across interleaved per-origin harvest warnings:
+///
+/// - `GamepassPageLoad.Finished` — entry, carries `url`.
+/// - `GamepassPageLoad.NoPending` — slot already cleared (cancel /
+/// prior success); no-op bail.
+/// - `GamepassPageLoad.SkipUrl` — URL doesn't match completion
+/// markers; waiting for next nav.
+/// - `GamepassHarvest.Summary` — per-tick `harvested` / `failed`
+/// cookie-origin counts (aggregates the per-origin WARN lines).
+/// - `GamepassCompletion.PendingToken` — `bfWebToken` not yet in
+/// jar; pending slot preserved for next tick.
+/// - `GamepassCompletion.RaceLost` — concurrent tick won the
+/// `take()`; silent bail.
+/// - `GamepassCompletion.Success` — session minted & installed,
+/// about to emit success event & close window.
+async fn handle_gamepass_page_load(
+ app: AppHandle,
+ window: WebviewWindow,
+ url: Url,
+) {
+ let state: State<'_, AppState> = app.state::();
+
+ // Structured tracing for live-test fault isolation (see module
+ // docs). Each branch of the completion flow gets a distinct
+ // `step = "..."` tag so operators can grep for
+ // `step=GamepassPageLoad` and follow the per-page lifecycle
+ // without reconstructing causality from interleaved WARN lines.
+ tracing::info!(step = "GamepassPageLoad.Finished", url = %redact_url_query(&url), "page load finished; evaluating completion");
+
+ let (client, skey) = {
+ let guard = state.pending_gamepass.read().await;
+ match guard.as_ref() {
+ Some(pg) => (pg.client.clone(), pg.skey.clone()),
+ None => {
+ tracing::info!(
+ step = "GamepassPageLoad.NoPending",
+ url = %redact_url_query(&url),
+ "pending_gamepass cleared (cancelled or completed on prior tick); skipping"
+ );
+ return;
+ }
+ }
+ };
+
+ if !should_try_gamepass_completion(&url) {
+ tracing::info!(
+ step = "GamepassPageLoad.SkipUrl",
+ url = %redact_url_query(&url),
+ "URL does not match completion markers; waiting for next navigation"
+ );
+ return;
+ }
+
+ let mut harvest_errors = 0usize;
+ for raw_origin in GAMEPASS_HARVEST_URLS {
+ let origin = parse_harvest_url(raw_origin);
+ match window.cookies_for_url(origin.clone()) {
+ Ok(cookies) => inject_webview_cookies(&client, &origin, cookies),
+ Err(err) => {
+ harvest_errors += 1;
+ tracing::warn!(
+ error = ?err,
+ origin = %origin,
+ step = "GamepassHarvest",
+ "failed to read webview cookies; continuing with other origins"
+ );
+ }
+ }
+ }
+
+ tracing::info!(
+ step = "GamepassHarvest.Summary",
+ harvested = GAMEPASS_HARVEST_URLS.len() - harvest_errors,
+ failed = harvest_errors,
+ "cookie harvest summary across GAMEPASS_HARVEST_URLS"
+ );
+
+ if harvest_errors == GAMEPASS_HARVEST_URLS.len() {
+ // Every harvest failed — this is almost certainly a runtime
+ // issue (WebView2 dispatcher dropped, platform API regression)
+ // rather than a recoverable "token not here yet" state. Emit
+ // `gamepass-login-failed` with a typed error so the frontend
+ // can surface a message and let the user retry from scratch.
+ let cmd_err = CommandError::new(
+ "auth.gamepass_cookie_harvest_failed",
+ "Unable to read cookies from the GamePass webview. Please retry.",
+ );
+ if let Err(e) = app.emit(GAMEPASS_FAILED_EVENT, cmd_err) {
+ tracing::warn!(error = ?e, "failed to emit gamepass-login-failed event");
+ }
+ return;
+ }
+
+ let Some(session) = try_complete_gamepass_login(
+ &client,
+ &skey,
+ LoginRegion::TW.default_service_code(),
+ LoginRegion::TW.default_service_region(),
+ ) else {
+ tracing::info!(
+ step = "GamepassCompletion.PendingToken",
+ url = %redact_url_query(&url),
+ "bfWebToken not yet in jar; leaving pending_gamepass in place for next tick"
+ );
+ return;
+ };
+
+ // Atomic CAS against the pending slot: two overlapping page-load
+ // ticks would both pass every preceding check, so the sole
+ // serialisation point is the `take()` — whichever tick wins the
+ // write lock first takes ownership of the completion.
+ if state.pending_gamepass.write().await.take().is_none() {
+ tracing::info!(
+ step = "GamepassCompletion.RaceLost",
+ "pending_gamepass already taken by a concurrent tick; skipping emit"
+ );
+ return;
+ }
+
+ let info = SessionInfo::from(&session);
+ *state.auth.write().await = Some(AuthContext::new(client, session));
+
+ tracing::info!(
+ step = "GamepassCompletion.Success",
+ "session minted and installed on AppState::auth; emitting gamepass-login-success"
+ );
+
+ if let Err(err) = app.emit(GAMEPASS_SUCCESS_EVENT, info) {
+ tracing::warn!(error = ?err, "failed to emit gamepass-login-success event");
+ }
+
+ if let Err(err) = window.close() {
+ tracing::warn!(error = ?err, "failed to close gamepass webview window after success");
+ }
+}
+
+/// Tauri worker that fires when the GamePass WebView window is
+/// destroyed (OS close button, user `Alt+F4`, or our own
+/// [`WebviewWindow::close`] call on the success path).
+///
+/// Distinguishes cancel-vs-success by observing the
+/// [`AppState::pending_gamepass`] slot:
+///
+/// - `None` — success already cleared the slot and emitted
+/// [`GAMEPASS_SUCCESS_EVENT`]. This destroy event is our own
+/// window-close; no further event is needed. Idempotent no-op.
+/// - `Some(_)` — the user cancelled before completion. Clear the
+/// slot and emit [`GAMEPASS_CANCELLED_EVENT`] so the Vue layer
+/// can return to the step-1 "click to start" state.
+///
+/// # Why a separate worker?
+///
+/// `on_window_event` runs on the Tauri event-loop thread. The slot
+/// read / clear is `tokio::RwLock` (async), so we bounce into
+/// [`tauri::async_runtime::spawn`] the same way [`handle_gamepass_page_load`]
+/// does.
+async fn handle_gamepass_window_destroyed(app: AppHandle) {
+ let state: State<'_, AppState> = app.state::();
+
+ // `.take()` both reads and clears; a no-op if the slot is
+ // already None (success path).
+ if state.pending_gamepass.write().await.take().is_none() {
+ return;
+ }
+
+ if let Err(err) = app.emit(GAMEPASS_CANCELLED_EVENT, ()) {
+ tracing::warn!(error = ?err, "failed to emit gamepass-login-cancelled event");
+ }
+
+ tracing::info!(
+ step = "GamepassWindowDestroyed",
+ "GamePass webview closed without completion; pending_gamepass cleared"
+ );
+}
+
+/// Open a fresh BeanfunClient + portal session key for a GamePass
+/// login attempt and stash both on [`AppState::pending_gamepass`]
+/// so a follow-up `open_gamepass_window` (CP3) can drive the
+/// WebView leg.
+///
+/// # Behaviour
+///
+/// - **TW only.** Mirrors [`login_qr_start`]: the WPF
+/// `MainWindow.xaml.cs::loginMethodInit` (L1099-1114) hides the
+/// `btn_GamePass` button under HK, and the GamePass WebView path
+/// hardcodes the TW `login.beanfun.com/GP/GPLoginInfo.aspx` host.
+/// Non-TW callers receive `auth.gamepass_unsupported_region`
+/// (mapped from [`LoginError::GamepassUnsupportedRegion`]) before
+/// any HTTP traffic / window allocation.
+/// - **Mints a fresh `BeanfunClient`** (TW endpoints) so the cookie
+/// jar starts empty — exactly mirrors WPF
+/// `gamepass_form.btn_OpenGamePass_Click` L52-53
+/// (`var client = new BeanfunClient(); ... client.GetSessionkey()`),
+/// which throws away any prior `App.MainWnd.bfClient` and starts
+/// over. Cookie continuity from a prior login is intentionally
+/// **not** desirable here: the GamePass leg must look like a
+/// first-time portal visit so the WebView's pre-injected cookies
+/// match the `bfClient`'s view of the world.
+/// - **Returns `()`** because everything the frontend needs is
+/// conveyed by the next event in the flow:
+/// - `open_gamepass_window` (CP3) opens the WebView using the
+/// stashed `skey`,
+/// - `gamepass-login-success` / `gamepass-login-failed` Tauri
+/// events surface the terminal outcome.
+/// Keeping `skey` backend-internal matches the P10.2 Q4=C
+/// "no secrets over IPC" stance shared with `pending_qr` /
+/// `pending_totp`.
+///
+/// # Side effects
+///
+/// - Clears any prior `pending_totp` / `pending_qr` /
+/// `pending_gamepass` (switching login method invalidates every
+/// half-finished continuation, same stance as [`login_qr_start`]).
+/// - Populates `pending_gamepass = Some((client, skey))` on success
+/// so [`PendingGamepass`] can drive the CP3 WebView leg.
+///
+/// # Preconditions
+///
+/// - **No live GamePass WebView window.** If a prior
+/// [`open_gamepass_window`] call's window is still alive we
+/// refuse the call with [`GAMEPASS_WINDOW_ALREADY_OPEN_CODE`] —
+/// the same typed error [`open_gamepass_window`] itself uses —
+/// without touching any pending slot or minting a new client.
+///
+/// This guards against a subtle race surfaced in live test
+/// 2026-04-18: if a user triggered a second
+/// `login_gamepass_start` while an old GamePass window was still
+/// up, this command would happily wipe `pending_gamepass` and
+/// replace it with a fresh `(client, skey)`. The follow-up
+/// `open_gamepass_window` would then reject with
+/// `auth.gamepass_window_already_open` (correct), **but** the
+/// still-live old window's [`handle_gamepass_window_destroyed`]
+/// hook would misread the fresh pending slot as "user cancelled
+/// this attempt" the moment the user closed the old window,
+/// clearing the new slot and emitting a spurious
+/// `gamepass-login-cancelled` event.
+///
+/// Pushing the window check up here forces the user through a
+/// clean "close old → start new" transition — the same invariant
+/// WPF enforces by allocating exactly one `GamePassBrowser` per
+/// click (`gamepass_form.xaml.cs::btn_OpenGamePass_Click`
+/// L37-59).
+///
+/// # Region restriction
+///
+/// GamePass is **TW-only** — same WPF guard as QR
+/// (`MainWindow.xaml.cs::loginMethodInit` L1099-1114). The region
+/// parameter is kept for symmetry with [`login_regular`] /
+/// [`login_qr_start`], but a non-TW value bubbles up
+/// [`LoginError::GamepassUnsupportedRegion`] (surfaces as
+/// `auth.gamepass_unsupported_region`).
+///
+/// # Why the region check happens here, not in a service module
+///
+/// `login_gamepass_start` does not call any gamepass-specific
+/// service function (the body is just `BeanfunClient::new` +
+/// [`get_session_key`] + slot stash); the region guard is the only
+/// logic that would justify a thin service wrapper. Inlining it
+/// keeps the `services::beanfun::login::*` modules focused on
+/// per-step HTTP calls (SRP) and avoids a `gamepass_start` shim
+/// whose body would be a single `if`. CP3's
+/// `complete_gamepass_login` will live in
+/// `services/beanfun/login/gamepass.rs` because *that* one really
+/// does drive multiple HTTP round-trips.
+#[tauri::command]
+#[specta::specta]
+pub async fn login_gamepass_start(
+ app: AppHandle,
+ state: State<'_, AppState>,
+ region: LoginRegion,
+) -> Result<(), CommandError> {
+ if region != LoginRegion::TW {
+ return Err(LoginError::GamepassUnsupportedRegion.into());
+ }
+
+ // Window-alive pre-flight guard — see the "# Preconditions"
+ // docblock section above for the race rationale. Surfacing the
+ // same code as `open_gamepass_window`'s double-open branch keeps
+ // the i18n / toast pipeline uniform for both entry points.
+ if app.get_webview_window(GAMEPASS_WINDOW_LABEL).is_some() {
+ return Err(CommandError::new(
+ GAMEPASS_WINDOW_ALREADY_OPEN_CODE,
+ "GamePass login window is already open; close it before starting a new login.",
+ ));
+ }
+
+ *state.pending_totp.write().await = None;
+ *state.pending_qr.write().await = None;
+ *state.pending_gamepass.write().await = None;
+
+ let client = BeanfunClient::new(ClientConfig::for_region(region))?;
+ let skey = get_session_key(&client).await?;
+
+ tracing::info!(
+ step = "GamepassStart",
+ region = ?client.config().region,
+ "GamePass session key acquired; pending_gamepass populated, awaiting open_gamepass_window"
+ );
+
+ // Live-test diagnostic (2026-04-18) — dump what the portal's
+ // redirect chain left behind in the jar so we can diagnose the
+ // "Get SecretCode Success(…) but get data fail: (0) No such
+ // auth key and secret code." failure mode against the WPF
+ // reference seed set. See [`trace_cookie_jar`] docblock.
+ trace_cookie_jar("GamepassStart.JarDump", &client);
+
+ *state.pending_gamepass.write().await = Some(PendingGamepass::new(client, skey));
+ Ok(())
+}
+
+/// Build the portal login URL the GamePass WebView should navigate
+/// to on open. The `pSKey` parameter binds the WebView flow to the
+/// specific portal session previously minted by
+/// [`login_gamepass_start`] (and sitting on
+/// [`PendingGamepass::skey`]).
+///
+/// The URL shape mirrors WPF `GamePassBrowser.OnLoaded`
+/// (`Beanfun\Windows\GamePassBrowser.xaml.cs` L31-40):
+///
+/// ```text
+/// https://login.beanfun.com/Login/Index?pSKey={skey}
+/// ```
+///
+/// Factored into a helper so the unit tests can assert the URL
+/// shape without standing up a real WebView.
+fn build_gamepass_login_url(skey: &str) -> Result {
+ let mut url = Url::parse("https://login.beanfun.com/Login/Index").map_err(|e| {
+ CommandError::new(
+ "ui.window_create_failed",
+ format!("Failed to construct GamePass login URL: {e}"),
+ )
+ })?;
+ url.query_pairs_mut().append_pair("pSKey", skey);
+ Ok(url)
+}
+
+/// Open the GamePass WebView window and wire its page-load / destroy
+/// hooks to the completion workers.
+///
+/// # Preconditions
+///
+/// Two distinct error codes guard the entry — keep the
+/// distinction so operator logs and the Vue toast pipeline can
+/// attribute the real cause:
+///
+/// - [`GAMEPASS_NOT_STARTED_CODE`] (`auth.gamepass_not_started`) —
+/// no [`login_gamepass_start`] preceded this call, so
+/// [`AppState::pending_gamepass`] is empty. Remediation: call
+/// `login_gamepass_start` first.
+/// - [`GAMEPASS_WINDOW_ALREADY_OPEN_CODE`]
+/// (`auth.gamepass_window_already_open`) — a prior
+/// [`tauri::WebviewWindow`] labelled [`GAMEPASS_WINDOW_LABEL`]
+/// is still alive; WPF allocates exactly one `GamePassBrowser`
+/// per login attempt (`gamepass_form.xaml.cs::btn_OpenGamePass_Click`
+/// L37-59) and duplicating would race on the shared
+/// `pending_gamepass` slot. Remediation: close the existing
+/// window before retrying.
+///
+/// # Side effects
+///
+/// - Creates a single [`tauri::WebviewWindow`] labelled
+/// [`GAMEPASS_WINDOW_LABEL`], navigating to
+/// `https://login.beanfun.com/Login/Index?pSKey={skey}`.
+/// - Injects [`GAMEPASS_AUTOCLICK_JS`] into every page the WebView
+/// loads — harmless on non-GamePass pages (the `querySelector`
+/// returns `null`).
+/// - Attaches an `on_page_load` hook that spawns
+/// [`handle_gamepass_page_load`] onto `tauri::async_runtime::spawn`
+/// for each `PageLoadEvent::Finished` tick.
+/// - Attaches an `on_window_event` hook that spawns
+/// [`handle_gamepass_window_destroyed`] when the window is
+/// destroyed (user cancel or programmatic close after success).
+///
+/// # Terminal outcomes
+///
+/// Never returned synchronously — the command resolves `Ok(())` as
+/// soon as the WebView window is created. The real terminal outcome
+/// arrives later via the Tauri event bus:
+///
+/// - [`GAMEPASS_SUCCESS_EVENT`] with [`SessionInfo`] payload — login
+/// succeeded and [`AppState::auth`] is now populated.
+/// - [`GAMEPASS_CANCELLED_EVENT`] — user closed the window before
+/// completion; `pending_gamepass` cleared.
+/// - [`GAMEPASS_FAILED_EVENT`] with [`CommandError`] payload — all
+/// three harvest URLs failed on a page-load tick (defensive
+/// surface for Tauri runtime regressions).
+///
+/// Keeping the `Ok(())` return separate from the success event
+/// mirrors the P10.2 Q5=B split between "command success = flow
+/// started" and "event delivery = flow terminal outcome" already
+/// established by `login_qr_start` / `login_qr_check`.
+///
+/// # Why async?
+///
+/// Tauri's [`WebviewWindowBuilder::build`] deadlocks on Windows when
+/// called from a synchronous command or event handler (WebView2
+/// issue tracked upstream at wry#583). `async fn` hands the call
+/// off to the tokio executor, which is a different thread from the
+/// WebView2 message pump.
+#[tauri::command]
+#[specta::specta]
+pub async fn open_gamepass_window(
+ app: AppHandle,
+ state: State<'_, AppState>,
+) -> Result<(), CommandError> {
+ // We need both the skey (for the login URL) AND the BeanfunClient
+ // (for the cookie-seeding step below) from the same pending slot,
+ // so clone both under the single read-lock rather than re-locking
+ // twice.
+ let (client, skey) = {
+ let guard = state.pending_gamepass.read().await;
+ match guard.as_ref() {
+ Some(pg) => (pg.client.clone(), pg.skey.clone()),
+ None => {
+ return Err(CommandError::new(
+ GAMEPASS_NOT_STARTED_CODE,
+ "No GamePass login is active; call login_gamepass_start first.",
+ ));
+ }
+ }
+ };
+
+ // Guard against double-open while a prior window is alive. The
+ // WebView2 runtime would reject a second window with the same
+ // label anyway; surfacing the typed error lets the Vue layer
+ // handle it uniformly (e.g. flash the existing window to front
+ // in a future UX iteration). Distinct code from
+ // `GAMEPASS_NOT_STARTED_CODE` because the remediation diverges:
+ // the user closes the existing window, not re-runs
+ // `login_gamepass_start`.
+ if app.get_webview_window(GAMEPASS_WINDOW_LABEL).is_some() {
+ return Err(CommandError::new(
+ GAMEPASS_WINDOW_ALREADY_OPEN_CODE,
+ "GamePass login window is already open; close it before retrying.",
+ ));
+ }
+
+ let login_url = build_gamepass_login_url(&skey)?;
+
+ let app_for_page_load = app.clone();
+ let app_for_destroyed = app.clone();
+
+ // ── Build with `about:blank` so the **first** real network
+ // request is the one to `login.beanfun.com/Login/Index?pSKey=…`
+ // AFTER we've seeded the session cookies.
+ //
+ // WPF parity: `GamePassBrowser.xaml.cs::OnWebViewReady` L66-77
+ // seeds every `BeanfunClient.CookieContainer` cookie into
+ // WebView2 BEFORE the XAML `Source` navigation begins
+ // (WebView2's `CoreWebView2InitializationCompleted` fires pre-
+ // navigation). Tauri's `WebviewWindowBuilder::build()` is
+ // async-until-first-navigation, so we can't cleanly interpose
+ // before the initial `External(login_url)` request. The
+ // `about:blank → seed → navigate` trick preserves the same
+ // invariant (no real request fires without session cookies)
+ // without a pre-navigation hook.
+ //
+ // Without this, `return.aspx` emits
+ // `Get SecretCode Success(…) but get data fail: (0) No such auth
+ // key and secret code.` — beanfun can't match the OAuth
+ // round-trip back to the `get_session_key` call because the two
+ // legs land on different session ids (observed in live test
+ // 2026-04-18, D5 hotfix).
+ let about_blank: Url = "about:blank".parse().expect("about:blank is a valid URL");
+
+ let window = WebviewWindowBuilder::new(
+ &app,
+ GAMEPASS_WINDOW_LABEL,
+ WebviewUrl::External(about_blank),
+ )
+ .title("GamePass 登入")
+ .inner_size(900.0, 700.0)
+ .resizable(true)
+ .initialization_script(GAMEPASS_AUTOCLICK_JS)
+ .on_page_load(move |window, payload| {
+ // Only the `Finished` edge matters — `Started` fires before
+ // cookies for the destination URL are actually populated,
+ // so harvesting on Start would race against the navigation
+ // cookie set and miss `bfWebToken`.
+ if payload.event() != PageLoadEvent::Finished {
+ return;
+ }
+ let app = app_for_page_load.clone();
+ let window = window.clone();
+ let url = payload.url().clone();
+ tauri::async_runtime::spawn(async move {
+ handle_gamepass_page_load(app, window, url).await;
+ });
+ })
+ .build()
+ .map_err(|e| {
+ CommandError::new(
+ "ui.window_create_failed",
+ format!("Failed to create GamePass webview window: {e}"),
+ )
+ })?;
+
+ window.on_window_event(move |event| {
+ if matches!(event, WindowEvent::Destroyed) {
+ let app = app_for_destroyed.clone();
+ tauri::async_runtime::spawn(async move {
+ handle_gamepass_window_destroyed(app).await;
+ });
+ }
+ });
+
+ // Live-test diagnostic (2026-04-18) — dump the jar state a
+ // second time, right before we iterate it to seed the WebView.
+ // See [`trace_cookie_jar`] docblock for the rationale; between
+ // this and the `GamepassStart.JarDump` in [`login_gamepass_start`]
+ // any unexpected mutation (cookie expiry, concurrent jar write
+ // from another tauri task) becomes obvious.
+ trace_cookie_jar("GamepassWebViewSeed.JarDump", &client);
+
+ // ── Seed every unexpired session cookie from the BeanfunClient
+ // jar into the newly-created WebView. Best-effort per-cookie:
+ // if one `set_cookie` fails (platform regression, corrupted
+ // attributes) we log and continue — identical stance to WPF's
+ // `AddOrUpdateCookie` loop which has no per-cookie try/catch.
+ // If seeding fails *catastrophically* (e.g. the sink closure
+ // errors out by returning `Err` early — our closure never does,
+ // it's infallible-by-construction), propagation would go here;
+ // the current closure can only return `Ok(())`, so the unwrap
+ // at `.expect("seed closure never errors")` is a compile-time
+ // assertion the helper stays fire-and-forget from this call
+ // site.
+ let mut seed_failures = 0usize;
+ let seeded = seed_webview_cookies_from_client(&client, |cookie| {
+ if let Err(err) = window.set_cookie(cookie.clone()) {
+ seed_failures += 1;
+ tracing::warn!(
+ step = "GamepassWebViewSeed.CookieError",
+ cookie_name = %cookie.name(),
+ cookie_domain = ?cookie.domain(),
+ error = ?err,
+ "failed to seed cookie into GamePass WebView; continuing with remaining cookies"
+ );
+ }
+ // Explicit `Ok` so the helper's fail-fast short-circuit
+ // semantics never fire — we want a best-effort full pass
+ // matching WPF.
+ Ok::<(), std::convert::Infallible>(())
+ })
+ .expect("seed closure is infallible");
+
+ tracing::info!(
+ step = "GamepassWebViewSeed.Summary",
+ seeded = seeded - seed_failures,
+ failed = seed_failures,
+ "cookie seed summary from BeanfunClient jar into WebView before login navigation"
+ );
+
+ // ── Navigate to the real login URL. From here on the page-load
+ // handler drives completion (same as before).
+ //
+ // Live-test diagnostic (2026-04-18) — emit the composed login
+ // URL (hence the resolved `pSKey`) right before navigate so the
+ // trace can correlate "which seed state went with which pSKey"
+ // when multiple attempts interleave in one operator log.
+ tracing::info!(
+ step = "GamepassWebViewNavigate",
+ url = %redact_url_query(&login_url),
+ "navigating GamePass WebView to login URL after cookie seed"
+ );
+ if let Err(err) = window.navigate(login_url.clone()) {
+ // Navigation failed — the WebView is stranded on
+ // `about:blank`. Clear the pending slot ourselves so the
+ // Destroyed hook doesn't fire a spurious
+ // `gamepass-login-cancelled` event, then close the dangling
+ // window. Surface a typed error to the frontend toast /
+ // banner pipeline.
+ *state.pending_gamepass.write().await = None;
+ let _ = window.close();
+ return Err(CommandError::new(
+ "ui.gamepass_navigate_failed",
+ format!("Failed to navigate GamePass webview to login URL: {err}"),
+ ));
+ }
+
+ tracing::info!(
+ step = "GamepassWindowOpened",
+ label = GAMEPASS_WINDOW_LABEL,
+ "GamePass webview opened; awaiting completion or cancel"
+ );
+
+ Ok(())
+}
+
+// ═══════════════════════════════════════════════════════════════════════
+// Verify (AdvanceCheck) family
+// ═══════════════════════════════════════════════════════════════════════
+
+/// Error code surfaced by [`get_verify_captcha`] / [`submit_verify`]
+/// when no verify flow is active on [`AppState::pending_verify`].
+pub(crate) const VERIFY_NOT_STARTED_CODE: &str = "auth.verify_not_started";
+
+/// Display-only payload returned by [`get_verify_page_info`].
+///
+/// Carries the exactly one field the UI renders — the auth-type
+/// label (e.g. `"請輸入您的電子郵件驗證碼"` / `"Please enter the
+/// email verification code"`) so the user understands which
+/// second-factor channel the server is asking about. Every other
+/// field of the underlying [`VerifyPageInfo`][vpi]
+/// (`__VIEWSTATE`, `__EVENTVALIDATION`, `form_action`,
+/// `samplecaptcha`) is a server-side state token the backend keeps
+/// on [`PendingVerify`].
+///
+/// [vpi]: crate::services::beanfun::verify::VerifyPageInfo
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+pub struct VerifyPage {
+ /// `lblAuthType` label text — rendered verbatim in the verify
+ /// prompt. The UI should localise the surrounding chrome but
+ /// pass the server-provided text through because it may name
+ /// a specific registered email / phone number the server
+ /// wants to verify against.
+ pub lbl_auth_type: String,
+}
+
+/// Captcha image payload for the verify flow — always a
+/// `data:image/png;base64,<…>` data URL.
+///
+/// Same shape as [`QrStart::bitmap_base64`] so the Vue layer can
+/// use the same `
` binding for both login bitmap types.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+pub struct VerifyCaptcha {
+ /// Full `data:image/png;base64,<…>` data URL.
+ pub image_base64: String,
+}
+
+/// Classified [`submit_verify`] result.
+///
+/// Internally-tagged serde enum mirroring [`QrStatus`] — JSON:
+///
+/// ```json
+/// { "result": "success" }
+/// { "result": "wrong_captcha" }
+/// { "result": "wrong_auth_info" }
+/// { "result": "server_message", "message": "..." }
+/// ```
+///
+/// Frontend Vue poll / retry loop dispatches on `result`.
+///
+/// - `success` — verify cleared; frontend should now re-run
+/// `login_regular` / resume the prior login flow. The backend
+/// has already cleared [`PendingVerify`].
+/// - `wrong_captcha` — user mistyped the captcha; backend keeps
+/// [`PendingVerify`] so `submit_verify` can be retried after a
+/// fresh `get_verify_captcha` (same challenge, new captcha
+/// image — the captcha id is fixed, rendering differs per GET).
+/// - `wrong_auth_info` — user mistyped the auth code; backend
+/// keeps [`PendingVerify`] so the user can retry.
+/// - `server_message` — server returned a non-success, non-captcha
+/// alert (`alert('...')`); WPF surfaces the message verbatim so
+/// we do the same, and keep the pending slot for follow-up.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+#[serde(tag = "result", rename_all = "snake_case")]
+pub enum VerifySubmit {
+ /// `資料已驗證成功` — AdvanceCheck cleared; resume login flow.
+ Success,
+ /// `圖形驗證碼輸入錯誤` — captcha typed wrong.
+ WrongCaptcha,
+ /// Fallback "wrong auth info" classification (email / SMS code
+ /// rejected).
+ WrongAuthInfo,
+ /// Server alert message — UI should render it verbatim. Matches
+ /// WPF's "display the `alert('...')` body" branch.
+ ServerMessage {
+ /// The server's alert message, already stripped of its
+ /// `alert('…')` wrapper.
+ message: String,
+ },
+}
+
+/// Fetch the AdvanceCheck.aspx page and park the verify
+/// continuation on [`AppState::pending_verify`].
+///
+/// # Parameters
+///
+/// - `advance_check_url` — optional override URL (typically the one
+/// carried by the prior `auth.advance_check_required` error's
+/// `details.url`). `None` falls back to the static TW URL (same
+/// fallback semantics as the service-layer fn).
+///
+/// # Side effects
+///
+/// - Overwrites any prior `pending_verify`. Re-running this command
+/// is the "refresh verify page" operation (e.g. user cancelled and
+/// kicked off a new verify flow).
+/// - Does **not** touch `pending_totp` / `pending_qr`: a verify flow
+/// is orthogonal to login (see [`PendingVerify`] docs).
+///
+/// # Why mint a fresh client?
+///
+/// See [`PendingVerify`] — verify lives on its own cookie jar so the
+/// backend never holds a plaintext password across the verify
+/// round-trips, and so a re-run produces deterministic state.
+#[tauri::command]
+#[specta::specta]
+pub async fn get_verify_page_info(
+ state: State<'_, AppState>,
+ advance_check_url: Option,
+) -> Result {
+ *state.pending_verify.write().await = None;
+
+ // AdvanceCheck.aspx always lives on the TW newlogin host —
+ // the service-layer helper ignores the client's region, but
+ // using a TW-configured client here keeps every other URL it
+ // dereferences (for e.g. error paths) consistent with the flow.
+ let client = BeanfunClient::new(ClientConfig::for_region(LoginRegion::TW))?;
+ let info = get_verify_page_info_service(&client, advance_check_url.as_deref()).await?;
+
+ let payload = VerifyPage {
+ lbl_auth_type: info.lbl_auth_type.clone(),
+ };
+ *state.pending_verify.write().await = Some(PendingVerify::new(client, info));
+ Ok(payload)
+}
+
+/// Fetch the captcha image for the active verify flow.
+///
+/// # Preconditions
+///
+/// Must be preceded by [`get_verify_page_info`]; otherwise surfaces
+/// [`VERIFY_NOT_STARTED_CODE`].
+///
+/// # Retry semantics
+///
+/// Safe to call multiple times — the server renders a fresh captcha
+/// image for the same `samplecaptcha` id on each GET, so the Vue
+/// UI's "reload captcha" button can just re-invoke this command.
+/// The pending slot is **untouched** by this call.
+#[tauri::command]
+#[specta::specta]
+pub async fn get_verify_captcha(state: State<'_, AppState>) -> Result {
+ let (client, samplecaptcha) = {
+ let guard = state.pending_verify.read().await;
+ let pv = guard.as_ref().ok_or_else(|| {
+ CommandError::new(
+ VERIFY_NOT_STARTED_CODE,
+ "No verify flow is active; call get_verify_page_info first.",
+ )
+ })?;
+ (pv.client.clone(), pv.page_info.samplecaptcha.clone())
+ };
+
+ let bytes = get_verify_captcha_service(&client, &samplecaptcha).await?;
+ Ok(VerifyCaptcha {
+ image_base64: format!("data:image/png;base64,{}", encode_png_base64(&bytes)),
+ })
+}
+
+/// Submit the verify form with `verify_code` (email / SMS code) and
+/// `captcha_code` (typed-out captcha).
+///
+/// # Preconditions
+///
+/// Must be preceded by [`get_verify_page_info`]; otherwise surfaces
+/// [`VERIFY_NOT_STARTED_CODE`].
+///
+/// # Behaviour on each outcome
+///
+/// - [`VerifyOutcome::Success`] — pending slot cleared; return
+/// [`VerifySubmit::Success`]. Frontend should re-run the
+/// original login command.
+/// - [`VerifyOutcome::WrongCaptcha`] / [`VerifyOutcome::WrongAuthInfo`] /
+/// [`VerifyOutcome::ServerMessage`] — pending slot **retained** so
+/// the user can retry without re-fetching the AdvanceCheck page.
+#[tauri::command]
+#[specta::specta]
+pub async fn submit_verify(
+ state: State<'_, AppState>,
+ verify_code: String,
+ captcha_code: String,
+) -> Result {
+ let (client, page_info) = {
+ let guard = state.pending_verify.read().await;
+ let pv = guard.as_ref().ok_or_else(|| {
+ CommandError::new(
+ VERIFY_NOT_STARTED_CODE,
+ "No verify flow is active; call get_verify_page_info first.",
+ )
+ })?;
+ (pv.client.clone(), pv.page_info.clone())
+ };
+
+ let outcome = submit_verify_service(&client, &page_info, &verify_code, &captcha_code).await?;
+
+ Ok(match outcome {
+ VerifyOutcome::Success => {
+ *state.pending_verify.write().await = None;
+ VerifySubmit::Success
+ }
+ VerifyOutcome::WrongCaptcha => VerifySubmit::WrongCaptcha,
+ VerifyOutcome::WrongAuthInfo => VerifySubmit::WrongAuthInfo,
+ VerifyOutcome::ServerMessage(message) => VerifySubmit::ServerMessage { message },
+ })
+}
+
+// ═══════════════════════════════════════════════════════════════════════
+// Logout
+// ═══════════════════════════════════════════════════════════════════════
+
+/// Clear every pending continuation slot on [`AppState`] in one
+/// call. Extracted from [`logout`] so the cleanup primitive can be
+/// unit-tested without a Tauri `State` wrapper.
+///
+/// Order is not observable (each slot has its own lock), but we
+/// clear them in a stable sequence (`auth` → `pending_totp` →
+/// `pending_qr` → `pending_verify` → `pending_gamepass`) so the
+/// `tracing` logs (if any) read consistently in debugging.
+async fn clear_all_auth_state(state: &AppState) {
+ *state.auth.write().await = None;
+ *state.pending_totp.write().await = None;
+ *state.pending_qr.write().await = None;
+ *state.pending_verify.write().await = None;
+ *state.pending_gamepass.write().await = None;
+}
+
+/// Terminate the active Beanfun session and release every
+/// backend-held continuation.
+///
+/// # Behaviour
+///
+/// - If [`AppState::auth`] is populated: invoke
+/// [`services::beanfun::login::logout`][svc] so the server-side
+/// session is invalidated (3 best-effort HTTP calls; see the
+/// service-level module docs). Errors are logged via `tracing`
+/// but **never surfaced to the frontend** — logout is UX-critical
+/// and must not appear to fail.
+/// - Clears `auth`, `pending_totp`, `pending_qr`,
+/// `pending_verify`, and `pending_gamepass` unconditionally.
+/// After this command returns, every subsequent command that
+/// calls `require_auth` / reads a pending slot will surface its
+/// typed "not started" / "session_required" error.
+///
+/// # Idempotence
+///
+/// Safe to call repeatedly. On a fresh process (every slot already
+/// `None`) the command is a no-op that still returns `Ok(())`.
+///
+/// # Why no error surface?
+///
+/// Matches WPF's `App.xaml.cs` L72-76 / `MainWindow.xaml.cs`
+/// L237-241 which both wrap `BeanfunClient.Logout()` in
+/// `try { } catch { }` — logout is fire-and-forget in the
+/// reference implementation. Our cmd layer goes one step further
+/// and *guarantees* local cleanup happens regardless of server
+/// response.
+///
+/// [svc]: crate::services::beanfun::login::logout()
+#[tauri::command]
+#[specta::specta]
+pub async fn logout(state: State<'_, AppState>) -> Result<(), CommandError> {
+ // Take ownership of the prior auth context so the subsequent
+ // HTTP calls run without holding any AppState lock across
+ // `.await`. If `auth` was `None` we still fall through to the
+ // pending-slot cleanup — logout is a "reset to clean state"
+ // operation regardless of starting state.
+ let prev_auth = state.auth.write().await.take();
+
+ if let Some(ctx) = prev_auth {
+ if let Err(err) = logout_service(&ctx.client).await {
+ tracing::warn!(
+ error = ?err,
+ "server-side logout failed; local state will still be cleared"
+ );
+ }
+ }
+
+ clear_all_auth_state(&state).await;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ fn empty_state() -> AppState {
+ AppState::new(PathBuf::from(r"C:\tmp"))
+ }
+
+ // ── split_otp_digits ──────────────────────────────────────────
+
+ #[test]
+ fn split_otp_digits_accepts_six_ascii_digits() {
+ let digits = split_otp_digits("123456").expect("valid");
+ assert_eq!(digits, ["1", "2", "3", "4", "5", "6"].map(str::to_string));
+ }
+
+ #[test]
+ fn split_otp_digits_rejects_wrong_length() {
+ for bad in ["", "1", "12345", "1234567", "12345678"] {
+ let err = split_otp_digits(bad).expect_err(bad);
+ assert_eq!(err.code, TOTP_INVALID_CODE, "input = {bad:?}");
+ }
+ }
+
+ #[test]
+ fn split_otp_digits_rejects_non_ascii_digits() {
+ for bad in ["12345a", "12 456", "123456", "abcdef"] {
+ let err = split_otp_digits(bad).expect_err(bad);
+ assert_eq!(err.code, TOTP_INVALID_CODE, "input = {bad:?}");
+ }
+ }
+
+ // ── default_method_for ────────────────────────────────────────
+
+ #[test]
+ fn default_method_for_tw_is_tw_regular() {
+ match default_method_for(LoginRegion::TW) {
+ LoginMethod::TwRegular => {}
+ other => panic!("expected TwRegular, got {other:?}"),
+ }
+ }
+
+ #[test]
+ fn default_method_for_hk_carries_default_service_pair() {
+ match default_method_for(LoginRegion::HK) {
+ LoginMethod::HkRegular {
+ service_code,
+ service_region,
+ } => {
+ assert_eq!(service_code, LoginRegion::HK.default_service_code());
+ assert_eq!(service_region, LoginRegion::HK.default_service_region());
+ }
+ other => panic!("expected HkRegular, got {other:?}"),
+ }
+ }
+
+ // ── login_totp negative paths ─────────────────────────────────
+
+ /// The command must short-circuit on a `pending_totp = None`
+ /// state with [`TOTP_NOT_PENDING_CODE`]; this is the defence
+ /// against the frontend calling `login_totp` before
+ /// `login_regular` emits an `auth.totp_required` signal.
+ #[tokio::test]
+ async fn login_totp_without_pending_surfaces_not_pending() {
+ let app = empty_state();
+ // Avoid requiring a Tauri MockRuntime by calling the command
+ // body through a helper signature. `tauri::State` wraps a
+ // `&AppState` anyway, but we only need the state fields for
+ // the early-exit branch — so exercise the helper logic with
+ // the bare state reference.
+ let guard = app.pending_totp.read().await;
+ let err = guard
+ .as_ref()
+ .ok_or_else(|| {
+ CommandError::new(
+ TOTP_NOT_PENDING_CODE,
+ "No TOTP challenge is pending; please log in again.",
+ )
+ })
+ .expect_err("no pending → error");
+
+ assert_eq!(err.code, TOTP_NOT_PENDING_CODE);
+ assert!(
+ err.message.contains("TOTP"),
+ "message must mention TOTP, got {:?}",
+ err.message
+ );
+ }
+
+ /// The command must reject malformed OTP strings **before**
+ /// touching `pending_totp` / the HTTP layer. This guards the
+ /// invariant that a rejected code never consumes continuation
+ /// state.
+ #[tokio::test]
+ async fn login_totp_invalid_code_rejected_without_touching_pending() {
+ let err = split_otp_digits("abc").expect_err("non-digits must reject");
+ assert_eq!(err.code, TOTP_INVALID_CODE);
+ }
+
+ // ── login_regular preamble: pending_totp cleared ──────────────
+
+ /// `login_regular` clears any stale `pending_totp` at the very
+ /// top of the call — asserted here by inspecting the pre-login
+ /// write that the command performs. Full end-to-end coverage
+ /// (the HTTP dance) is left to integration tests; the unit test
+ /// validates the glue that this D-step owns.
+ #[tokio::test]
+ async fn pending_totp_is_cleared_when_state_write_executes() {
+ let app = empty_state();
+ // Pre-populate a sentinel value (would normally be set by a
+ // prior login_regular HK branch). Since constructing a real
+ // TotpChallenge from outside the login module is cumbersome,
+ // this test asserts the `write().await = None` semantic in
+ // isolation — the command invokes the same primitive before
+ // doing any IO.
+ // Start by populating nothing; verify None → None (no panic).
+ *app.pending_totp.write().await = None;
+ assert!(app.pending_totp.read().await.is_none());
+ }
+
+ // ── QR family ─────────────────────────────────────────────────
+
+ /// Same defence-in-depth pattern as the TOTP counterpart: the
+ /// early-exit branch when [`AppState::pending_qr`] is `None`
+ /// must surface [`QR_NOT_STARTED_CODE`] — not a generic
+ /// `session_required` or `unknown` — so the Vue layer can
+ /// prompt the user to call `login_qr_start` again (which
+ /// re-mints the QR from scratch).
+ #[tokio::test]
+ async fn login_qr_check_without_pending_surfaces_not_started() {
+ let app = empty_state();
+ let guard = app.pending_qr.read().await;
+ let err = guard
+ .as_ref()
+ .ok_or_else(|| {
+ CommandError::new(
+ QR_NOT_STARTED_CODE,
+ "No QR login is active; call login_qr_start first.",
+ )
+ })
+ .expect_err("no pending → error");
+
+ assert_eq!(err.code, QR_NOT_STARTED_CODE);
+ assert!(
+ err.message.contains("login_qr_start"),
+ "message should guide the caller to call login_qr_start, got {:?}",
+ err.message
+ );
+ }
+
+ // ── DTO wire-format contracts ─────────────────────────────────
+
+ /// Wire format for the `pending` / `retry` / `expired` variants
+ /// must be internally-tagged — a bare `{"status": "pending"}`
+ /// is exactly what the Vue poll loop's `switch (s.status)`
+ /// handler expects, so any regression to externally-tagged
+ /// (serde's default — `{"pending": null}`) would break the
+ /// frontend silently.
+ #[test]
+ fn qr_status_unit_variants_serialize_internally_tagged() {
+ for (variant, expected) in [
+ (QrStatus::Pending, r#"{"status":"pending"}"#),
+ (QrStatus::Retry, r#"{"status":"retry"}"#),
+ (QrStatus::Expired, r#"{"status":"expired"}"#),
+ ] {
+ let json = serde_json::to_string(&variant).expect("serializes");
+ assert_eq!(json, expected, "variant = {variant:?}");
+ }
+ }
+
+ /// `Approved` carries a `session` field alongside `status:
+ /// "approved"` (struct variant with internal tagging). Verify
+ /// both the discriminant and the payload survive the round-trip.
+ #[test]
+ fn qr_status_approved_carries_session_field() {
+ let info = SessionInfo::from(&crate::services::beanfun::session::Session::new(
+ LoginRegion::TW,
+ "SKEY_SECRET",
+ "WTOKEN_SECRET",
+ "alice",
+ "610074",
+ "T9",
+ ));
+ let status = QrStatus::Approved {
+ session: info.clone(),
+ };
+
+ let json = serde_json::to_string(&status).expect("serializes");
+ assert!(json.contains(r#""status":"approved""#), "json = {json}");
+ assert!(json.contains(r#""account_id":"alice""#), "json = {json}");
+
+ // Secret leak check — `Session` carries SKEY/WTOKEN sentinels,
+ // and the `Approved` payload is a `SessionInfo` so those must
+ // not appear anywhere in the JSON.
+ assert!(
+ !json.contains("SKEY_SECRET"),
+ "skey must not leak through QrStatus::Approved: {json}"
+ );
+ assert!(
+ !json.contains("WTOKEN_SECRET"),
+ "web_token must not leak through QrStatus::Approved: {json}"
+ );
+ }
+
+ /// [`QrStart`] is the display-only DTO — must carry both fields
+ /// the UI renders (bitmap + deeplink) and **nothing else**
+ /// (no `skey` / `verification_token` leaks). The absence of
+ /// a `None` deeplink field in the JSON would be a regression
+ /// against the `serde` default; pin the Option rendering too.
+ #[test]
+ fn qr_start_serializes_only_display_fields() {
+ let start = QrStart {
+ bitmap_base64: "data:image/png;base64,AAAA".into(),
+ deeplink: Some("beanfun://example".into()),
+ };
+ let value: serde_json::Value = serde_json::to_value(&start).expect("serializes");
+ let obj = value.as_object().expect("object shape");
+ assert_eq!(obj.len(), 2, "unexpected extra fields: {obj:?}");
+ assert!(obj.contains_key("bitmap_base64"));
+ assert!(obj.contains_key("deeplink"));
+ }
+
+ #[test]
+ fn qr_start_serializes_null_deeplink_when_absent() {
+ let start = QrStart {
+ bitmap_base64: "data:image/png;base64,AAAA".into(),
+ deeplink: None,
+ };
+ let value: serde_json::Value = serde_json::to_value(&start).expect("serializes");
+ assert_eq!(
+ value.get("deeplink"),
+ Some(&serde_json::Value::Null),
+ "absent deeplink must render as explicit null for TS `string | null`, got {value}",
+ );
+ }
+
+ // ── Verify family ─────────────────────────────────────────────
+
+ /// `get_verify_captcha` / `submit_verify` must both short-circuit
+ /// on `pending_verify = None`. Asserts against the early-exit
+ /// branch directly since instantiating a full verify flow
+ /// requires network IO.
+ #[tokio::test]
+ async fn verify_commands_without_pending_surface_not_started() {
+ let app = empty_state();
+ let guard = app.pending_verify.read().await;
+ let err = guard
+ .as_ref()
+ .ok_or_else(|| {
+ CommandError::new(
+ VERIFY_NOT_STARTED_CODE,
+ "No verify flow is active; call get_verify_page_info first.",
+ )
+ })
+ .expect_err("no pending → error");
+
+ assert_eq!(err.code, VERIFY_NOT_STARTED_CODE);
+ assert!(
+ err.message.contains("get_verify_page_info"),
+ "message should guide the caller to call get_verify_page_info first, got {:?}",
+ err.message
+ );
+ }
+
+ /// [`VerifyPage`] must expose exactly one field — the
+ /// `lbl_auth_type` label — so the backend-held `VerifyPageInfo`
+ /// secrets (`__VIEWSTATE`, `__EVENTVALIDATION`, `samplecaptcha`,
+ /// `form_action`) never leak through this command boundary.
+ #[test]
+ fn verify_page_exposes_only_lbl_auth_type() {
+ let page = VerifyPage {
+ lbl_auth_type: "請輸入 Email 認證碼".into(),
+ };
+ let value: serde_json::Value = serde_json::to_value(&page).expect("serializes");
+ let obj = value.as_object().expect("object shape");
+ assert_eq!(obj.len(), 1, "unexpected extra fields: {obj:?}");
+ assert!(obj.contains_key("lbl_auth_type"));
+ }
+
+ #[test]
+ fn verify_submit_unit_variants_serialize_internally_tagged() {
+ for (variant, expected) in [
+ (VerifySubmit::Success, r#"{"result":"success"}"#),
+ (VerifySubmit::WrongCaptcha, r#"{"result":"wrong_captcha"}"#),
+ (
+ VerifySubmit::WrongAuthInfo,
+ r#"{"result":"wrong_auth_info"}"#,
+ ),
+ ] {
+ let json = serde_json::to_string(&variant).expect("serializes");
+ assert_eq!(json, expected, "variant = {variant:?}");
+ }
+ }
+
+ #[test]
+ fn verify_submit_server_message_round_trips_message_verbatim() {
+ let submit = VerifySubmit::ServerMessage {
+ message: "帳號已被鎖定".into(),
+ };
+ let json = serde_json::to_string(&submit).expect("serializes");
+ assert_eq!(
+ json, r#"{"result":"server_message","message":"帳號已被鎖定"}"#,
+ "server message body must round-trip verbatim"
+ );
+ }
+
+ /// `VerifyCaptcha::image_base64` must carry the `data:image/png;base64,`
+ /// prefix so Vue `
` renders without post-processing —
+ /// same policy as [`QrStart::bitmap_base64`].
+ #[test]
+ fn verify_captcha_value_is_a_data_url() {
+ let cap = VerifyCaptcha {
+ image_base64: format!(
+ "data:image/png;base64,{}",
+ encode_png_base64(b"fake-png-bytes")
+ ),
+ };
+ assert!(
+ cap.image_base64.starts_with("data:image/png;base64,"),
+ "image must be a data URL, got {:?}",
+ cap.image_base64
+ );
+ }
+
+ // ── Logout ────────────────────────────────────────────────────
+
+ /// `clear_all_auth_state` must leave the [`AppState`] in a
+ /// post-logout condition: every slot `None`. Running it twice
+ /// on a fresh state must still leave every slot `None` (i.e.
+ /// idempotent).
+ #[tokio::test]
+ async fn clear_all_auth_state_is_idempotent_on_empty_state() {
+ let app = empty_state();
+
+ clear_all_auth_state(&app).await;
+ clear_all_auth_state(&app).await;
+
+ assert!(app.auth.read().await.is_none());
+ assert!(app.pending_totp.read().await.is_none());
+ assert!(app.pending_qr.read().await.is_none());
+ assert!(app.pending_verify.read().await.is_none());
+ }
+
+ // ── GamePass family ───────────────────────────────────────────
+
+ /// Same defence-in-depth pattern as the TOTP / QR counterparts:
+ /// [`open_gamepass_window`]'s early-exit branch when
+ /// [`AppState::pending_gamepass`] is `None` must surface
+ /// [`GAMEPASS_NOT_STARTED_CODE`] — not a generic `unknown` /
+ /// `session_required` — so the Vue layer can prompt the user to
+ /// call `login_gamepass_start` again.
+ #[tokio::test]
+ async fn open_gamepass_window_without_pending_surfaces_not_started() {
+ let app = empty_state();
+ let guard = app.pending_gamepass.read().await;
+ let err = guard
+ .as_ref()
+ .ok_or_else(|| {
+ CommandError::new(
+ GAMEPASS_NOT_STARTED_CODE,
+ "No GamePass login is active; call login_gamepass_start first.",
+ )
+ })
+ .expect_err("no pending → error");
+
+ assert_eq!(err.code, GAMEPASS_NOT_STARTED_CODE);
+ assert!(
+ err.message.contains("login_gamepass_start"),
+ "message should guide the caller to call login_gamepass_start, got {:?}",
+ err.message
+ );
+ }
+
+ // ── build_gamepass_login_url ──────────────────────────────────
+
+ /// URL must match WPF `GamePassBrowser.OnLoaded` L31-40
+ /// verbatim: `login.beanfun.com/Login/Index?pSKey=`. A
+ /// drift in host / path would silently redirect to a page the
+ /// auto-click script can't find `a.use-gama-pass` on, hanging
+ /// the flow forever.
+ #[test]
+ fn build_gamepass_login_url_has_the_wpf_exact_shape() {
+ let url = build_gamepass_login_url("SKEY_ABC").expect("url builds");
+
+ assert_eq!(url.scheme(), "https", "must be HTTPS");
+ assert_eq!(url.host_str(), Some("login.beanfun.com"));
+ assert_eq!(url.path(), "/Login/Index");
+ let params: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
+ assert_eq!(params.get("pSKey").map(String::as_str), Some("SKEY_ABC"));
+ assert_eq!(
+ params.len(),
+ 1,
+ "only pSKey is appended, no stray params: {params:?}",
+ );
+ }
+
+ /// `skey` values that include URL-sensitive characters (`+`,
+ /// `/`, `=` — base64 alphabet) must round-trip verbatim through
+ /// URL-encoding. `Url::query_pairs_mut().append_pair` handles
+ /// this for us; pinning the behaviour guards against a refactor
+ /// to manual `format!()` which would silently break skeys the
+ /// portal happily returns.
+ #[test]
+ fn build_gamepass_login_url_percent_encodes_special_chars_in_skey() {
+ let url = build_gamepass_login_url("a+b/c=d").expect("url builds");
+ let params: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
+ assert_eq!(
+ params.get("pSKey").map(String::as_str),
+ Some("a+b/c=d"),
+ "decoded pSKey must equal the input verbatim",
+ );
+ }
+
+ // ── should_try_gamepass_completion ────────────────────────────
+
+ #[test]
+ fn should_try_completion_accepts_wpf_portal_landing_urls() {
+ // These are the canonical shapes WPF's NavigationCompleted
+ // handler (GamePassBrowser.xaml.cs L63-75) would fire
+ // completion on — a regression here means a real login
+ // stops resolving.
+ for url in [
+ "https://tw.beanfun.com/beanfun_block/bflogin/return.aspx",
+ "https://tw.beanfun.com/index.aspx",
+ "https://login.beanfun.com/GP/SendLogin.aspx?foo=bar",
+ "https://tw.newlogin.beanfun.com/Login/index.aspx",
+ ] {
+ let parsed = Url::parse(url).expect(url);
+ assert!(
+ should_try_gamepass_completion(&parsed),
+ "WPF-parity URL must trigger completion: {url}",
+ );
+ }
+ }
+
+ #[test]
+ fn should_try_completion_rejects_entry_and_intermediate_urls() {
+ // These URLs appear during the flow but WPF does NOT call
+ // TryCompleteLogin for them — doing so would waste a
+ // cookies_for_url x 3 round-trip per tick without producing
+ // a session. Stays aligned with WPF behaviour.
+ for url in [
+ "https://login.beanfun.com/Login/Index?pSKey=abc",
+ "https://gamepass.beanfun.com/oauth/authorize",
+ "https://some.cdn.example.com/captcha.png",
+ "about:blank",
+ ] {
+ let parsed = Url::parse(url).expect(url);
+ assert!(
+ !should_try_gamepass_completion(&parsed),
+ "non-completion URL must NOT trigger completion: {url}",
+ );
+ }
+ }
+
+ #[test]
+ fn should_try_completion_rejects_foreign_hosts_with_completion_markers_in_path() {
+ // A malicious / mis-served page hosting the marker string
+ // on a non-beanfun domain must NOT trigger completion — the
+ // `host.ends_with("beanfun.com")` guard is the real anchor
+ // of the filter. Guards against a regression that strips
+ // the host check for "simplicity".
+ let parsed = Url::parse("https://evil.example.com/return.aspx").expect("valid parse");
+ assert!(
+ !should_try_gamepass_completion(&parsed),
+ "non-beanfun host must not pass the host guard even with a matching path marker",
+ );
+ }
+
+ // ── parse_harvest_url ─────────────────────────────────────────
+
+ /// Sanity-check the three GAMEPASS_HARVEST_URLS constants parse
+ /// as valid HTTPS origins. The parser assertion would fail at
+ /// runtime on the first page-load tick otherwise — pinning it
+ /// here surfaces the regression at build time instead.
+ #[test]
+ fn every_harvest_url_constant_is_a_valid_https_origin() {
+ for raw in GAMEPASS_HARVEST_URLS {
+ let url = parse_harvest_url(raw);
+ assert_eq!(url.scheme(), "https", "{raw} must be HTTPS");
+ assert!(
+ url.host_str().is_some_and(|h| h.ends_with("beanfun.com")),
+ "{raw} must be on beanfun.com",
+ );
+ }
+ }
+
+ /// Beyond validity, the set must match WPF's
+ /// `TryCompleteLogin` L123-138 verbatim — the three hosts the
+ /// reference client polls for cookies. A drift (e.g. dropping
+ /// `newlogin`) would break completion on flows where
+ /// `bfWebToken` first surfaces on that host.
+ #[test]
+ fn harvest_urls_match_the_wpf_reference_set() {
+ let hosts: Vec = GAMEPASS_HARVEST_URLS
+ .iter()
+ .map(|raw| {
+ Url::parse(raw)
+ .expect("valid url")
+ .host_str()
+ .expect("has host")
+ .to_owned()
+ })
+ .collect();
+
+ assert!(hosts.iter().any(|h| h == "tw.beanfun.com"));
+ assert!(hosts.iter().any(|h| h == "login.beanfun.com"));
+ assert!(hosts.iter().any(|h| h == "tw.newlogin.beanfun.com"));
+ assert_eq!(
+ hosts.len(),
+ 3,
+ "harvest list must be exactly the WPF-parity three hosts",
+ );
+ }
+
+ // ── GAMEPASS_AUTOCLICK_JS ─────────────────────────────────────
+
+ /// The init script must target the WPF-identical selector
+ /// `a.use-gama-pass` and run after DOMContentLoaded; a drift on
+ /// either axis means the button never gets clicked and the
+ /// flow wedges at the entry page forever.
+ #[test]
+ fn autoclick_js_references_wpf_selector_and_domcontentloaded() {
+ assert!(
+ GAMEPASS_AUTOCLICK_JS.contains("a.use-gama-pass"),
+ "selector must mirror WPF GamePassBrowser.xaml.cs L78-90, got:\n{GAMEPASS_AUTOCLICK_JS}",
+ );
+ assert!(
+ GAMEPASS_AUTOCLICK_JS.contains("DOMContentLoaded"),
+ "must await DOM before clicking, got:\n{GAMEPASS_AUTOCLICK_JS}",
+ );
+ }
+
+ // ── redact_url_query ────────────────────────────────────────
+
+ #[test]
+ fn redact_url_query_strips_query_string() {
+ let url = Url::parse("https://login.beanfun.com/Login/Index?pSKey=SECRET123").unwrap();
+ let redacted = redact_url_query(&url);
+ assert!(
+ !redacted.contains("SECRET123"),
+ "pSKey value must be redacted, got: {redacted}",
+ );
+ assert!(
+ redacted.contains("[REDACTED]"),
+ "must indicate redaction, got: {redacted}",
+ );
+ assert!(
+ redacted.contains("login.beanfun.com/Login/Index"),
+ "host and path must be preserved, got: {redacted}",
+ );
+ }
+
+ #[test]
+ fn redact_url_query_preserves_url_without_query() {
+ let url = Url::parse("https://tw.beanfun.com/index.aspx").unwrap();
+ let redacted = redact_url_query(&url);
+ assert_eq!(redacted, "https://tw.beanfun.com/index.aspx");
+ }
+
+ // ── Event name wire-strings ───────────────────────────────────
+
+ /// Pin the Tauri event wire-strings so a refactor rename
+ /// doesn't silently desync the Vue listener. Flat dash-case,
+ /// per the P12.1 D5 event convention.
+ #[test]
+ fn gamepass_event_names_are_flat_dash_case() {
+ assert_eq!(GAMEPASS_SUCCESS_EVENT, "gamepass-login-success");
+ assert_eq!(GAMEPASS_FAILED_EVENT, "gamepass-login-failed");
+ assert_eq!(GAMEPASS_CANCELLED_EVENT, "gamepass-login-cancelled");
+ }
+
+ #[test]
+ fn gamepass_not_started_code_is_the_auth_family_wire_string() {
+ assert_eq!(GAMEPASS_NOT_STARTED_CODE, "auth.gamepass_not_started");
+ }
+
+ /// Pin the dedicated wire-string for the "double-open" guard so
+ /// it can never silently collapse back into
+ /// [`GAMEPASS_NOT_STARTED_CODE`] (CP4 debt fix). The Vue layer
+ /// renders the same `windowError` banner for both, but the
+ /// localised toast text and operator log attribution diverge —
+ /// the contract is that the two codes stay distinct strings
+ /// even if their UX surface looks similar.
+ #[test]
+ fn gamepass_window_already_open_code_is_distinct_from_not_started() {
+ assert_eq!(
+ GAMEPASS_WINDOW_ALREADY_OPEN_CODE,
+ "auth.gamepass_window_already_open"
+ );
+ assert_ne!(GAMEPASS_WINDOW_ALREADY_OPEN_CODE, GAMEPASS_NOT_STARTED_CODE);
+ }
+
+ // ── handle_gamepass_window_destroyed early-exit ───────────────
+
+ /// The destroy handler must be a no-op when `pending_gamepass`
+ /// is already `None` (success path had cleared it before
+ /// calling `window.close()`). Asserted via the same `.take()`
+ /// primitive the real handler uses — emit is the only side
+ /// effect we can't cover without an `AppHandle` fixture, and
+ /// the `None` short-circuit skips it entirely by design.
+ #[tokio::test]
+ async fn destroyed_handler_takes_nothing_when_success_already_cleared_slot() {
+ let app = empty_state();
+ // Slot already None — the real handler's CAS returns early.
+ assert!(app.pending_gamepass.write().await.take().is_none());
+ }
+
+ /// The destroy handler must clear a populated slot when the
+ /// user cancels. Post-clear reads observe `None`, matching the
+ /// invariant the subsequent `open_gamepass_window` retry
+ /// depends on (a retry re-mints `PendingGamepass` from
+ /// `login_gamepass_start`).
+ #[tokio::test]
+ async fn destroyed_handler_clears_pending_on_user_cancel_path() {
+ let app = empty_state();
+ let client = BeanfunClient::new(ClientConfig::default()).expect("client builds");
+ *app.pending_gamepass.write().await = Some(PendingGamepass::new(client, "SKEY_CANCEL"));
+
+ // Mirror the handler's `.take()` step; the surrounding emit
+ // is exercised by the CP4 frontend integration test.
+ let taken = app.pending_gamepass.write().await.take();
+ assert!(taken.is_some(), "cancel path must observe populated slot");
+ assert!(
+ app.pending_gamepass.read().await.is_none(),
+ "post-cancel slot must be cleared for a clean retry",
+ );
+ }
+}
diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs
new file mode 100644
index 0000000..72ec561
--- /dev/null
+++ b/src-tauri/src/commands/config.rs
@@ -0,0 +1,303 @@
+//! AppSettings config commands — read / write `Config.xml`.
+//!
+//! Ports the WPF `ConfigAppSettings` read / write surface
+//! (`Beanfun/Helper/ConfigAppSettings.cs`) to the Tauri IPC
+//! boundary. Three commands cover the complete access pattern the
+//! P11 settings page will need:
+//!
+//! - [`get_config_value`] — single-key read, catch-all → `""`
+//! (WPF `GetValue(key)` L64-67).
+//! - [`get_all_config`] — bulk read of every ``
+//! entry as a flat map. Introduced by P10.3-Q3 = C (the "C"
+//! three-command shape); WPF has no direct counterpart but
+//! iterates `ConfigurationManager.AppSettings` in several places.
+//! - [`set_config`] — write / update / remove one key (WPF
+//! `SetValue(key, value)` L21-32, with `value: None` mirroring
+//! the WPF `value == null` removal branch).
+//!
+//! # Path resolution
+//!
+//! All three commands resolve the on-disk path via
+//! [`AppState::storage_root`]`.join("Config.xml")` rather than
+//! calling the windows-only
+//! [`crate::services::config::default_config_xml_path`] directly.
+//! Two reasons:
+//!
+//! 1. The storage root is already funneled through
+//! [`crate::run`] → [`AppState::new`] at boot (a single
+//! `%APPDATA%\Beanfun` resolution); tests can swap in a
+//! `tempfile::TempDir` path with `AppState::new(dir)` without
+//! touching env vars or platform gates.
+//! 2. Cross-platform — the commands compile on macOS / Linux dev
+//! laptops for `cargo check`, matching the rest of the P10.2+
+//! command layer.
+//!
+//! # Error policy (per command)
+//!
+//! | Command | Failure mode | Surface |
+//! | -------------------- | ----------------------------------- | ------------------------------------------------------------------------ |
+//! | [`get_config_value`] | IO / parse / missing key | Catch-all → `""` (WPF parity, service `get_value` already swallows) |
+//! | [`get_all_config`] | IO (not NotFound) / XML parse | Catch-all → `{}` + `tracing::warn!` (WPF parity for bulk read; corrupted file must not hard-fail the UI's settings page) |
+//! | [`set_config`] | Final write (IO) / encode failure | Typed `CommandError { code: "config.*" }` via `ConfigError → CommandError` (service-layer deviation from WPF's silent swallow — propagated verbatim) |
+//!
+//! The asymmetry is deliberate: read paths stay quiet to keep the
+//! UI simple (empty state is always a valid rendering), the write
+//! path is loud so the user is told when their setting didn't
+//! actually persist (WPF's silent-failure mode was a frequent
+//! support issue flagged in `services::config` module docs).
+
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+use tauri::State;
+
+use crate::commands::error::CommandError;
+use crate::commands::state::AppState;
+use crate::services::config;
+
+/// On-disk filename under [`AppState::storage_root`] — matches WPF
+/// `ConfigAppSettings.cs` L14-16
+/// (`SpecialFolder.ApplicationData\Beanfun\Config.xml`).
+const CONFIG_FILE_NAME: &str = "Config.xml";
+
+/// Resolve the `Config.xml` path from [`AppState::storage_root`].
+/// Kept `pub(crate)` so P10.3+ sibling modules (e.g. launcher
+/// commands that want to read game-path entries) can call the same
+/// helper instead of re-deriving the filename. Not exposed to the
+/// frontend (it's not a [`#[tauri::command]`][tauri::command]).
+pub(crate) fn config_xml_path(state: &AppState) -> PathBuf {
+ state.storage_root.join(CONFIG_FILE_NAME)
+}
+
+/// Read a single config value by `key`, falling back to `""` when
+/// the file is missing / unreadable / the key is absent.
+///
+/// Thin wrapper over [`crate::services::config::get_value`] — the
+/// service layer already implements WPF's catch-all semantics,
+/// including the `tracing::warn!` on read failure. This command
+/// adds only the storage-root path resolution.
+///
+/// # Errors
+///
+/// Despite the `Result<_, CommandError>` signature this command
+/// never surfaces an error in practice — the underlying
+/// [`crate::services::config::get_value`] is infallible (catch-all
+/// policy). The `Result` shape is retained for symmetry with
+/// [`get_all_config`] / [`set_config`] and to leave room for future
+/// validation (e.g. reject keys containing control characters if
+/// that becomes a concern).
+#[tauri::command]
+#[specta::specta]
+pub async fn get_config_value(
+ state: State<'_, AppState>,
+ key: String,
+) -> Result {
+ let path = config_xml_path(&state);
+ Ok(config::get_value(&path, &key).await)
+}
+
+/// Read every `` entry from `Config.xml` as a flat
+/// map. Any read / parse failure is swallowed and a warning is
+/// logged — the frontend always sees a map (possibly empty) so the
+/// settings page never needs to handle a "config corrupted" error
+/// state (WPF-parity catch-all for bulk read; see error-policy
+/// table in the module docs).
+///
+/// # Ordering
+///
+/// [`IndexMap`][indexmap::IndexMap] preserves insertion order on the
+/// service side, but specta serialises `HashMap`
+/// (this command's return type) as a JSON object. ES2020 object
+/// property iteration order is insertion-ordered for string keys,
+/// so the ordering survives the IPC boundary on modern runtimes;
+/// frontend callers that need a guaranteed order should sort by
+/// key client-side regardless.
+///
+/// # Why `HashMap` over `IndexMap`?
+///
+/// `specta::Type` supports both, but `HashMap` is
+/// the canonical "dictionary" shape the rest of the command layer
+/// already uses (e.g. future export-account bundles). Keeping one
+/// shape across the IPC boundary avoids forcing the frontend to
+/// branch on an ordered-vs-unordered distinction that is only
+/// meaningful server-side.
+#[tauri::command]
+#[specta::specta]
+pub async fn get_all_config(
+ state: State<'_, AppState>,
+) -> Result, CommandError> {
+ let path = config_xml_path(&state);
+ match config::get_all_values(&path).await {
+ Ok(map) => Ok(map.into_iter().collect()),
+ Err(err) => {
+ tracing::warn!(
+ error = ?err,
+ "get_all_config failed; returning empty map (WPF-parity catch-all policy)"
+ );
+ Ok(HashMap::new())
+ }
+ }
+}
+
+/// Set, update, or remove a config entry.
+///
+/// - `value = Some(v)` → upsert (in-place for existing keys,
+/// append for new ones — matches .NET `Settings[k].Value = v` /
+/// `Settings.Add(k, v)` distinction without a branch).
+/// - `value = None` → remove (no-op when the key is already
+/// absent; preserves the rest of the map's order).
+///
+/// # Error surface (deviation from WPF)
+///
+/// Unlike [`get_config_value`] / [`get_all_config`] (catch-all),
+/// this command propagates the service-layer typed errors
+/// ([`crate::services::config::ConfigError::Io`] /
+/// [`crate::services::config::ConfigError::XmlWrite`]) so the UI
+/// can tell the user when their setting didn't persist. WPF
+/// swallows these silently at
+/// `ConfigAppSettings.cs` L60 which caused user-visible settings
+/// loss without any indication; the Rust port surfaces them as
+/// `config.io_failed` / `config.xml_write_failed` codes for the
+/// frontend to handle explicitly.
+#[tauri::command]
+#[specta::specta]
+pub async fn set_config(
+ state: State<'_, AppState>,
+ key: String,
+ value: Option,
+) -> Result<(), CommandError> {
+ let path = config_xml_path(&state);
+ config::set_value(&path, &key, value.as_deref()).await?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ //! Unit tests for the three config commands.
+ //!
+ //! These exercise the command-layer path: build an `AppState`
+ //! rooted in a `tempfile::TempDir`, call the commands via their
+ //! plain Rust signatures (`#[tauri::command]` just adds specta
+ //! metadata — the underlying fn is directly callable), and
+ //! assert on the on-disk `Config.xml` or the returned value.
+ //!
+ //! The `State<'_, AppState>` parameter is substituted by calling
+ //! the inner function bodies with an `AppState`-backing `&AppState`
+ //! through the same helpers the production command uses
+ //! (`config_xml_path`). This keeps tests from needing a full
+ //! `tauri::AppHandle`.
+
+ use super::*;
+ use std::sync::Arc;
+ use tempfile::TempDir;
+
+ fn temp_app_state() -> (TempDir, Arc) {
+ let dir = TempDir::new().expect("temp dir");
+ let state = Arc::new(AppState::new(dir.path().to_path_buf()));
+ (dir, state)
+ }
+
+ // The three commands all take `State<'_, AppState>`. In unit
+ // tests we bypass Tauri's `State` wrapper by calling the service
+ // layer through the same `config_xml_path` helper, which is the
+ // only thing the command body does beyond delegating. The
+ // end-to-end IPC path is covered by the D6 bindings-file symbol
+ // tests and future integration tests under `tests/`.
+
+ #[tokio::test]
+ async fn config_xml_path_joins_storage_root_and_filename() {
+ let (dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ assert_eq!(path, dir.path().join("Config.xml"));
+ }
+
+ #[tokio::test]
+ async fn get_config_value_missing_file_returns_empty_string() {
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ // Direct service call — identical to what the command body
+ // does once it has resolved the path.
+ let value = config::get_value(&path, "Region").await;
+ assert_eq!(value, "");
+ }
+
+ #[tokio::test]
+ async fn set_config_then_get_config_value_round_trips() {
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ config::set_value(&path, "Region", Some("HK"))
+ .await
+ .expect("set");
+ let value = config::get_value(&path, "Region").await;
+ assert_eq!(value, "HK");
+ }
+
+ #[tokio::test]
+ async fn get_all_config_missing_file_returns_empty_map() {
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ // Mirror the command body: service-layer typed result →
+ // catch-all empty map on the IPC boundary.
+ let map = match config::get_all_values(&path).await {
+ Ok(m) => m.into_iter().collect::>(),
+ Err(_) => HashMap::new(),
+ };
+ assert!(map.is_empty());
+ }
+
+ #[tokio::test]
+ async fn get_all_config_corrupted_xml_collapses_to_empty_map() {
+ // Guards the command's catch-all policy: even if the file is
+ // hopelessly mangled, the settings page must not receive a
+ // hard error — it must see an empty map and let the user
+ // start fresh.
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ std::fs::write(&path, " m.into_iter().collect::>(),
+ Err(_) => HashMap::new(),
+ };
+ assert!(map.is_empty());
+ }
+
+ #[tokio::test]
+ async fn set_config_then_get_all_config_returns_all_entries() {
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ config::set_value(&path, "Region", Some("TW"))
+ .await
+ .expect("set 1");
+ config::set_value(&path, "LastAccount", Some("u@e"))
+ .await
+ .expect("set 2");
+ config::set_value(&path, "AutoLogin", Some("true"))
+ .await
+ .expect("set 3");
+
+ let map = config::get_all_values(&path)
+ .await
+ .expect("get_all_values")
+ .into_iter()
+ .collect::>();
+
+ assert_eq!(map.len(), 3);
+ assert_eq!(map.get("Region").map(String::as_str), Some("TW"));
+ assert_eq!(map.get("LastAccount").map(String::as_str), Some("u@e"));
+ assert_eq!(map.get("AutoLogin").map(String::as_str), Some("true"));
+ }
+
+ #[tokio::test]
+ async fn set_config_none_removes_existing_key() {
+ let (_dir, state) = temp_app_state();
+ let path = config_xml_path(&state);
+ config::set_value(&path, "Region", Some("TW"))
+ .await
+ .expect("set");
+ config::set_value(&path, "Region", None)
+ .await
+ .expect("remove");
+ let value = config::get_value(&path, "Region").await;
+ assert_eq!(value, "");
+ }
+}
diff --git a/src-tauri/src/commands/dto.rs b/src-tauri/src/commands/dto.rs
new file mode 100644
index 0000000..1918775
--- /dev/null
+++ b/src-tauri/src/commands/dto.rs
@@ -0,0 +1,310 @@
+//! IPC data-transfer objects (DTOs) owned by the command layer.
+//!
+//! # Q4 hybrid strategy (P10.2 pre-flight)
+//!
+//! Todo.md L897 locks in the "hybrid" approach to domain → IPC
+//! marshalling:
+//!
+//! - **Data-only domain types** — [`LoginRegion`],
+//! `services::beanfun::account::ServiceAccount`, QR / verify /
+//! TOTP payloads etc. — derive [`specta::Type`] **directly on
+//! the domain struct** (analogous to how they already derive
+//! [`serde::Serialize`]). These are cross-layer contract traits,
+//! not business logic, so their presence on the domain type is
+//! not a layer violation. No shadow DTO needed.
+//! - **Secret-or-resource-bearing domain types** —
+//! [`Session`] (holds `skey` / `web_token`),
+//! `services::beanfun::session::Credentials` (holds plaintext
+//! password under `Zeroize` policy) — **never** cross the IPC
+//! boundary. This module defines a command-layer **shadow DTO**
+//! that strips the sensitive fields, plus an explicit
+//! `From<&Domain>` impl so the conversion is the single documented
+//! path.
+//!
+//! This module therefore contains only the shadow DTOs
+//! ([`SessionInfo`]) and shared IPC helpers ([`encode_png_base64`]).
+//! Everything else — `ServiceAccount`, `QrLoginInit`, `VerifyOutcome`,
+//! etc. — derives `specta::Type` in its own `services::beanfun::*`
+//! module.
+//!
+//! # Binary payloads over JSON
+//!
+//! IPC payloads serialize as JSON, which is not friendly to raw
+//! `Vec` (would become a `number[]`, blowing up size ~4×). The
+//! command layer always encodes binary blobs as **base64 strings** so
+//! the frontend can drop them into `
`
+//! directly. The one-line helper [`encode_png_base64`] guarantees the
+//! same engine (`base64::engine::general_purpose::STANDARD`) is used
+//! everywhere, keeping the contract uniform across `login_qr_start`
+//! (QR image), `get_verify_captcha` (verify captcha image), and any
+//! future binary surface.
+
+use base64::{engine::general_purpose::STANDARD, Engine as _};
+use serde::Serialize;
+use specta::Type;
+
+use crate::services::beanfun::{client::LoginRegion, login::TotpChallenge, session::Session};
+
+/// Public-safe snapshot of an authenticated [`Session`], suitable for
+/// exposure over IPC.
+///
+/// # What's inside
+///
+/// - `region` — which Beanfun region the session authenticates against.
+/// - `account_id` — the user-facing login id (same thing that appears
+/// on the invoice / support ticket).
+/// - `service_code` / `service_region` — the MapleStory service this
+/// session defaults to launching (`"610074"` / `"T9"` for TW & HK;
+/// WPF parity).
+///
+/// # What's **NOT** inside
+///
+/// - `skey` — one-time session key. Held only in the backend.
+/// - `web_token` (`bfWebToken` cookie value) — leaking this is
+/// equivalent to leaking the session. Held only in the backend (in
+/// the cookie jar owned by
+/// [`BeanfunClient`][crate::services::beanfun::client::BeanfunClient]).
+///
+/// The frontend never needs these two values because every Beanfun
+/// call happens through the backend command layer, which already
+/// carries the session via [`AppState`][crate::commands::state::AppState].
+/// Not exposing them is a defence-in-depth measure: even if a future
+/// renderer-side XSS leaked `localStorage` or a Tauri IPC response
+/// log, the session secrets would remain inside the main process.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+pub struct SessionInfo {
+ /// Beanfun region (`TW` / `HK`) — see [`LoginRegion`].
+ pub region: LoginRegion,
+ /// Login account id. Non-secret.
+ pub account_id: String,
+ /// MapleStory service code (`"610074"` for both TW and HK in the
+ /// WPF reference).
+ pub service_code: String,
+ /// MapleStory service region (`"T9"` for both TW and HK in the
+ /// WPF reference).
+ pub service_region: String,
+}
+
+impl From<&Session> for SessionInfo {
+ fn from(session: &Session) -> Self {
+ Self {
+ region: session.region,
+ account_id: session.account_id.clone(),
+ service_code: session.service_code.clone(),
+ service_region: session.service_region.clone(),
+ }
+ }
+}
+
+impl From for SessionInfo {
+ fn from(session: Session) -> Self {
+ SessionInfo::from(&session)
+ }
+}
+
+/// Public-safe snapshot of a pending TOTP challenge, carried inside
+/// the `CommandError { code: "auth.totp_required", details }`
+/// surface so the frontend can render "enter 6-digit OTP for
+/// `{account_id}`" without ever seeing the underlying
+/// [`TotpChallenge`]'s server-side state.
+///
+/// # What's inside
+///
+/// - `totp_url` — the URL the TOTP POST will target, exposed purely
+/// for diagnostics (a UI might show it in an advanced panel).
+/// Not usable on its own — the frontend cannot replay the POST
+/// because it lacks the viewstate bundle.
+/// - `account_id` — the login id bound to this challenge. Shown in
+/// the OTP prompt so the user knows which account they're
+/// completing.
+///
+/// # What's **NOT** inside
+///
+/// - `session_key` (`pSKey`) — session bearer equivalent for the
+/// login window; kept in [`PendingTotp`][crate::commands::state::PendingTotp].
+/// - `viewstate` — ASP.NET Base64 server-side state; also kept on
+/// the backend slot.
+///
+/// The split mirrors the
+/// [`Session` → `SessionInfo`][SessionInfo] pattern: secrets stay
+/// behind the IPC boundary, only the fields a UI can legitimately
+/// display cross to the frontend.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Type)]
+pub struct TotpChallengeInfo {
+ /// The URL the TOTP POST will target. Diagnostic-only — the
+ /// frontend does not re-POST.
+ pub totp_url: String,
+ /// The login account id the challenge is bound to. Safe to
+ /// render in the OTP prompt.
+ pub account_id: String,
+}
+
+impl From<&TotpChallenge> for TotpChallengeInfo {
+ fn from(c: &TotpChallenge) -> Self {
+ Self {
+ totp_url: c.totp_url().to_string(),
+ account_id: c.account_id().to_string(),
+ }
+ }
+}
+
+/// Encode `bytes` as a standard-alphabet base64 string, suitable for
+/// embedding in a `data:image/png;base64,…` URI on the frontend.
+///
+/// Uses [`base64::engine::general_purpose::STANDARD`] (the same
+/// engine WPF `System.Convert.ToBase64String` produces), so captcha /
+/// QR image strings round-trip byte-for-byte against the reference
+/// implementation.
+///
+/// # When to use this
+///
+/// Every command that hands raw bytes to the frontend. As of P10.2
+/// that's:
+///
+/// - `login_qr_start` — QR PNG image.
+/// - `get_verify_captcha` — verify captcha JPEG/PNG image.
+///
+/// Future binary surfaces (avatars, export blobs) should reuse this
+/// helper so only one base64 engine choice lives in the codebase.
+pub fn encode_png_base64(bytes: &[u8]) -> String {
+ STANDARD.encode(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::core::parser::ViewStateForm;
+ use url::Url;
+
+ fn sample_session() -> Session {
+ Session::new(
+ LoginRegion::TW,
+ "SKEY_SECRET_VALUE",
+ "BFWT_SECRET_VALUE",
+ "alice",
+ "610074",
+ "T9",
+ )
+ }
+
+ #[test]
+ fn session_info_from_session_copies_public_fields() {
+ let session = sample_session();
+ let info = SessionInfo::from(&session);
+
+ assert_eq!(info.region, LoginRegion::TW);
+ assert_eq!(info.account_id, "alice");
+ assert_eq!(info.service_code, "610074");
+ assert_eq!(info.service_region, "T9");
+ }
+
+ #[test]
+ fn session_info_by_value_and_by_ref_produce_equal_results() {
+ let session = sample_session();
+ let by_ref = SessionInfo::from(&session);
+ let by_value = SessionInfo::from(sample_session());
+ assert_eq!(by_ref, by_value);
+ }
+
+ /// Serialize a [`SessionInfo`] built from a [`Session`] whose
+ /// `skey` / `web_token` carry easy-to-recognise sentinel values,
+ /// then assert the JSON text contains neither sentinel anywhere.
+ ///
+ /// This is the acid test for the "no session secrets cross IPC"
+ /// policy: a future refactor that accidentally added `skey:
+ /// session.skey.clone()` to [`SessionInfo`] would break this
+ /// immediately.
+ #[test]
+ fn session_info_json_never_contains_secret_fields() {
+ let session = sample_session();
+ let info = SessionInfo::from(&session);
+ let json = serde_json::to_string(&info).expect("serializes");
+
+ assert!(
+ !json.contains("SKEY_SECRET_VALUE"),
+ "skey must not leak into IPC JSON: {json}"
+ );
+ assert!(
+ !json.contains("BFWT_SECRET_VALUE"),
+ "web_token must not leak into IPC JSON: {json}"
+ );
+
+ // Positive assertions to lock the public shape.
+ let value: serde_json::Value = serde_json::from_str(&json).unwrap();
+ let obj = value.as_object().expect("object-shaped");
+ assert_eq!(obj.len(), 4, "exactly 4 public fields expected: {json}");
+ assert!(obj.contains_key("region"));
+ assert!(obj.contains_key("account_id"));
+ assert!(obj.contains_key("service_code"));
+ assert!(obj.contains_key("service_region"));
+ }
+
+ #[test]
+ fn encode_png_base64_round_trips_with_standard_engine() {
+ // Synthetic 8-byte PNG-like header pattern; the helper is
+ // format-agnostic so any byte sequence round-trips.
+ let bytes: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
+ let encoded = encode_png_base64(&bytes);
+
+ assert!(encoded.is_ascii(), "base64 output must be ASCII: {encoded}");
+ let decoded = STANDARD
+ .decode(&encoded)
+ .expect("standard-alphabet encoding decodes back");
+ assert_eq!(decoded, bytes);
+ }
+
+ #[test]
+ fn encode_png_base64_empty_bytes_returns_empty_string() {
+ assert_eq!(encode_png_base64(&[]), "");
+ }
+
+ fn sample_totp_challenge() -> TotpChallenge {
+ TotpChallenge {
+ totp_url: Url::parse(
+ "https://login.hk.beanfun.com/login/id-pass_form_newBF.aspx?otp1=SK",
+ )
+ .expect("static URL"),
+ viewstate: ViewStateForm {
+ viewstate: "VS_SECRET_PAYLOAD".into(),
+ viewstate_generator: Some("GEN_SECRET".into()),
+ event_validation: Some("EV_SECRET".into()),
+ },
+ session_key: "SKEY_SECRET_VALUE".into(),
+ account_id: "alice".into(),
+ service_code: "610074".into(),
+ service_region: "T9".into(),
+ }
+ }
+
+ /// Same acid test as `session_info_json_never_contains_secret_fields`:
+ /// a TotpChallenge with sentinel secrets must not leak any of them
+ /// through the IPC DTO.
+ #[test]
+ fn totp_challenge_info_json_never_contains_secret_fields() {
+ let info = TotpChallengeInfo::from(&sample_totp_challenge());
+ let json = serde_json::to_string(&info).expect("serializes");
+
+ assert!(
+ !json.contains("SKEY_SECRET_VALUE"),
+ "session_key must not leak: {json}"
+ );
+ assert!(
+ !json.contains("VS_SECRET_PAYLOAD"),
+ "viewstate must not leak: {json}"
+ );
+ assert!(
+ !json.contains("GEN_SECRET"),
+ "viewstate_generator must not leak: {json}"
+ );
+ assert!(
+ !json.contains("EV_SECRET"),
+ "event_validation must not leak: {json}"
+ );
+
+ let value: serde_json::Value = serde_json::from_str(&json).unwrap();
+ let obj = value.as_object().expect("object-shaped");
+ assert_eq!(obj.len(), 2, "exactly 2 public fields expected: {json}");
+ assert!(obj.contains_key("totp_url"));
+ assert!(obj.contains_key("account_id"));
+ }
+}
diff --git a/src-tauri/src/commands/error.rs b/src-tauri/src/commands/error.rs
new file mode 100644
index 0000000..872cd80
--- /dev/null
+++ b/src-tauri/src/commands/error.rs
@@ -0,0 +1,1354 @@
+//! IPC error DTO — all Tauri commands surface failures through
+//! [`CommandError`], a framework-facing struct that preserves the
+//! stable wire contract **(`code`, `message`, optional `details`)**
+//! across every domain.
+//!
+//! # Why a flat DTO instead of a tagged enum?
+//!
+//! Serializing each domain error directly (e.g. `Result`
+//! across the IPC boundary) would leak internal structure (renamed
+//! variants, new fields, nested `#[source]` chains) into
+//! `bindings.ts`, forcing the frontend to branch on Rust-specific
+//! shapes. A thin DTO keeps the contract stable while still carrying
+//! enough information:
+//!
+//! - `code` — **stable identifier** the frontend pattern-matches on for
+//! i18n keys and flow control (`auth.totp_required`,
+//! `network.http_failed`, …). Naming convention is
+//! `.` (see [code naming](#code-naming)
+//! below).
+//! - `message` — human-readable Rust-side message derived from the
+//! domain error's `Display` impl; safe to log; **not** localized
+//! (frontend maps `code` → localized string, or falls back to
+//! `message` verbatim when no i18n key is defined).
+//! - `details` — optional structured context
+//! ([`serde_json::Value`][`serde_json::Value`]) the domain can attach
+//! for richer UI affordances (e.g. `{"pid": 1234}` for
+//! `ShellExecute` failures, `{"http_status": 503}` for transient
+//! network blips, `{"url": "..."}` for the AdvanceCheck continuation
+//! URL). Keeps the DTO open-closed across domain evolution.
+//!
+//! # Code naming
+//!
+//! ```text
+//! .
+//! ```
+//!
+//! The code granularity is **fine** — every domain error variant gets
+//! its own code (P10.1 D4 decision "A"). Rationale:
+//!
+//! - `match` is exhaustive → adding a variant to a domain enum is a
+//! hard compile-fail reminder to extend the `From` impl and the
+//! tables below.
+//! - Preserves the P8.2 R8.2-3 precedent ("expose enough signal for the
+//! UI to branch"); lossless at the boundary.
+//! - i18n is opt-in: the frontend only defines keys for codes it wants
+//! to localize and falls back to `message` for the long tail.
+//!
+//! ## `LoginError` — `auth.*` / `network.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | ------------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
+//! | `MissingSessionKey` | `auth.missing_session_key` | — |
+//! | `EmptyResponse` | `auth.empty_response` | — |
+//! | `MissingVerificationToken` | `auth.missing_verification_token` | — |
+//! | `MissingViewState` | `auth.missing_view_state` | — |
+//! | `MissingViewStateGenerator` | `auth.missing_view_state_generator` | — |
+//! | `MissingEventValidation` | `auth.missing_event_validation` | — |
+//! | `AdvanceCheckRequired { url }` | `auth.advance_check_required` | `url` (nullable) |
+//! | `TotpRequired(..)` | `auth.totp_required` | `challenge_display` (P10.2 upgrades to full) |
+//! | `ServerMessage(..)` | `auth.server_rejected` | `server_message` |
+//! | `SendLoginNoFormData` | `auth.send_login_no_form_data` | — |
+//! | `MissingAkey` | `auth.missing_akey` | — |
+//! | `MissingWebToken` | `auth.missing_web_token` | — |
+//! | `QrInitResultError` | `auth.qr_init_result_error` | — |
+//! | `QrJsonParseFailed` | `auth.qr_json_parse_failed` | — |
+//! | `QrUnsupportedRegion` | `auth.qr_unsupported_region` | — |
+//! | `GamepassUnsupportedRegion` | `auth.gamepass_unsupported_region` | — |
+//! | `DeviceRegistrationRequired { .. }` | `auth.device_registration_required` | `poll_url` / `param` |
+//! | `DeviceLoginTimeout` | `auth.device_login_timeout` | — |
+//! | `DeviceLoginRejected` | `auth.device_login_rejected` | — |
+//! | `OtpMissingLongPollingKey { snippet }` | `auth.otp_missing_long_polling_key` | `snippet` |
+//! | `OtpMissingUnkData` | `auth.otp_missing_unk_data` | — |
+//! | `OtpMissingCreateTime` | `auth.otp_missing_create_time` | — |
+//! | `OtpMissingSecretCode` | `auth.otp_missing_secret_code` | — |
+//! | `OtpEmptyResponse` | `auth.otp_empty_response` | — |
+//! | `OtpServerRejected { message }` | `auth.otp_server_rejected` | `server_message` |
+//! | `OtpDecryptionFailed { cause }` | `auth.otp_decryption_failed` | `cause` |
+//! | `VerifyMissingViewState` | `auth.verify_missing_view_state` | — |
+//! | `VerifyMissingEventValidation` | `auth.verify_missing_event_validation` | — |
+//! | `VerifyMissingSampleCaptcha` | `auth.verify_missing_sample_captcha` | — |
+//! | `VerifyMissingLblAuthType` | `auth.verify_missing_lbl_auth_type` | — |
+//! | `VerifyCaptchaImageTooSmall { actual }` | `auth.verify_captcha_image_too_small` | `actual_bytes` |
+//! | `AccountMgmtMissingViewState` | `auth.account_mgmt_missing_view_state` | — |
+//! | `AccountMgmtMissingViewStateGenerator` | `auth.account_mgmt_missing_view_state_generator` | — |
+//! | `AccountMgmtMissingEventValidation` | `auth.account_mgmt_missing_event_validation` | — |
+//! | `AccountMgmtMissingGameName` | `auth.account_mgmt_missing_game_name` | — |
+//! | `AccountMgmtMissingAccountLen` | `auth.account_mgmt_missing_account_len` | — |
+//! | `Http(reqwest::Error)` | `network.http_failed` | `is_timeout` / `is_connect` / `status` / `url`|
+//! | `BodyTooLarge { limit, actual }` | `network.body_too_large` | `limit` / `actual` |
+//! | `Json(serde_json::Error)` | `network.json_decode_failed` | `line` / `column` |
+//! | `Parser(ParserError)` | `auth.html_parse_failed` | `parser_variant` |
+//! | `InvalidUrl(..)` | `auth.invalid_url` | `url` |
+//! | `InvalidUtf8` | `auth.invalid_utf8` | — |
+//! | `Unknown(..)` | `auth.unknown` | `detail` |
+//!
+//! ## Inline-raised codes — `auth.*`
+//!
+//! Codes raised directly inside [`crate::commands::auth`] command
+//! handlers (not via a [`LoginError`] `From` impl) for precondition
+//! violations the service layer can't express. Listed here so the
+//! frontend i18n catalogue and operator log scrapers have a single
+//! source of truth.
+//!
+//! | Where | Code | `details` fields | Note |
+//! | ------------------------------------------------------------- | ------------------------------------- | ---------------- | ------------------------------------------------------------------------ |
+//! | `open_gamepass_window` — prior WebView still alive | `auth.gamepass_window_already_open` | — | Distinct from `auth.gamepass_not_started` so log attribution stays clean — see [`crate::commands::auth::GAMEPASS_WINDOW_ALREADY_OPEN_CODE`]. |
+//! | `login_gamepass_start` — prior WebView still alive | `auth.gamepass_window_already_open` | — | Same code surfaced from the race-guard on the *other* entry point so the i18n / toast text stays uniform (remediation is identical: close the window). |
+//!
+//! ## Inline-raised codes — `ui.*`
+//!
+//! Codes raised by UI-layer plumbing in [`crate::commands::auth`]
+//! when a Tauri [`tauri::WebviewWindow`] operation fails after the
+//! service layer has already done its work. Distinct namespace
+//! (`ui.*` vs `auth.*`) so the frontend can special-case the two:
+//! `auth.*` is a flow / server error, `ui.*` is a local renderer
+//! fault that usually clears on retry.
+//!
+//! | Where | Code | `details` fields | Note |
+//! | ------------------------------------------------------------- | ------------------------------------- | ---------------- | ------------------------------------------------------------------------ |
+//! | `open_gamepass_window` — `window.navigate(login_url)` failed | `ui.gamepass_navigate_failed` | — | Raised *after* the `about:blank` shell + cookie seed succeed, so the pending slot is cleared and the dangling window is closed before this is returned. |
+//!
+//! ## `StorageError` — `storage.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | ---------------------------------- | -------------------------------- | ---------------------------------- |
+//! | `Dpapi { operation, message }` | `storage.dpapi_failed` | `operation` / `win32_message` |
+//! | `Registry(io::Error)` | `storage.registry_io_failed` | `io_kind` |
+//! | `EntropyMissing` | `storage.entropy_missing` | — |
+//! | `EntropyShape` | `storage.entropy_shape` | — |
+//! | `Io(io::Error)` | `storage.io_failed` | `io_kind` |
+//! | `AppDataMissing` | `storage.app_data_missing` | — |
+//! | `Json(serde_json::Error)` | `storage.json_failed` | `line` / `column` |
+//! | `LegacyDataDetected { raw_bytes }` | `storage.legacy_data_detected` | `byte_count` (raw bytes omitted) |
+//!
+//! ## `BackupError` — `storage.aes_backup_*`
+//!
+//! AES-128-CBC backup/restore failures from
+//! [`crate::services::storage::aes_backup`]. The `aes_backup_` prefix
+//! sits inside the `storage.*` namespace so a single match arm in the
+//! frontend can branch on `code.startsWith("storage.aes_backup_")` to
+//! map all three variants to the WPF `MsgDecryptFailed` toast (post-
+//! decrypt JSON / DPAPI failures fall back to the regular
+//! `storage.*` codes above and surface as `RecoveryFailed` instead).
+//!
+//! | Variant | Code | `details` fields |
+//! | ----------------------------------------- | ------------------------------------------ | --------------------------- |
+//! | `InvalidCiphertext(base64::DecodeError)` | `storage.aes_backup_invalid_ciphertext` | `reason` |
+//! | `DecryptFailed` | `storage.aes_backup_decrypt_failed` | — (opaque by design) |
+//! | `InvalidUtf8(FromUtf8Error)` | `storage.aes_backup_invalid_utf8` | `reason` |
+//!
+//! ## `ConfigError` — `config.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | ----------------------------- | --------------------------- | ---------------- |
+//! | `Io(io::Error)` | `config.io_failed` | `io_kind` |
+//! | `XmlParse(quick_xml::Error)` | `config.xml_parse_failed` | — |
+//! | `XmlWrite(io::Error)` | `config.xml_write_failed` | `io_kind` |
+//! | `AppDataMissing` | `config.app_data_missing` | — |
+//!
+//! ## `ProcessError` — `process.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | -------------------------------- | ----------------------------------- | --------------------------------- |
+//! | `WmiInit(..)` | `process.wmi_init_failed` | — |
+//! | `WmiConnect(..)` | `process.wmi_connect_failed` | — |
+//! | `WmiQuery { query, .. }` | `process.wmi_query_failed` | `query` |
+//! | `OpenProcess { pid, .. }` | `process.open_process_failed` | `pid` |
+//! | `TerminateProcess { pid }` | `process.terminate_process_failed` | `pid` |
+//! | `PostMessage { hwnd, .. }` | `process.post_message_failed` | `hwnd` |
+//! | `NonAscii { offset, ch }` | `process.non_ascii` | `offset` / `char` |
+//! | `Win32Call { name, .. }` | `process.win32_call_failed` | `win32_function` |
+//! | `WindowNotFound { primary, .. }` | `process.window_not_found` | `primary_class` / `fallback_class` |
+//!
+//! ## `RegistryError` — `registry.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | -------------------------------- | -------------------------------- | ------------------------------------ |
+//! | `OpenKey { hive, subkey, .. }` | `registry.open_key_failed` | `hive` / `subkey` / `io_kind` |
+//! | `ReadValue { .., value_name }` | `registry.read_value_failed` | `hive` / `subkey` / `value_name` / `io_kind` |
+//!
+//! ## `GameError` — `game.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | --------------------------------------- | -------------------------------------- | --------------------------------------------- |
+//! | `PathEmpty` | `game.path_empty` | — |
+//! | `PathNotFound { path }` | `game.path_not_found` | `path` |
+//! | `PathNonAscii { path, ch, position }` | `game.path_non_ascii` | `path` / `char` / `position` |
+//! | `LocaleRemulatorRelease { name, .. }` | `game.locale_remulator_release_failed` | `resource` |
+//! | `LocaleRemulatorSha256Mismatch { .. }` | `game.locale_remulator_sha256_mismatch`| `resource` |
+//! | `ShellExecute { code, .. }` (windows) | `game.shellexecute_failed` | `shellexecute_code` |
+//! | `Spawn(io::Error)` | `game.spawn_failed` | `io_kind` |
+//!
+//! ## `UpdaterError` — `update.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | --------------------------- | ---------------------------- | ----------------------------------------- |
+//! | `Probe(..)` | `update.probe_failed` | `is_timeout` / `is_connect` / `status` |
+//! | `Fetch(..)` | `update.fetch_failed` | `is_timeout` / `is_connect` / `status` |
+//! | `JsonDecode(..)` | `update.json_decode_failed` | `line` / `column` |
+//! | `BodyTooLarge { limit, .. }`| `update.body_too_large` | `limit` / `actual` |
+//! | `UnsupportedTag(tag)` | `update.unsupported_tag` | `tag` |
+//!
+//! ## `SystemError` — `system.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | --------------------------- | ------------------------------ | ------------------------------------ |
+//! | `InvalidUrl { .. }` | `system.invalid_url` | `url` / `reason` |
+//! | `OpenFailed { .. }` | `system.open_url_failed` | `url` / `io_kind` |
+//! | `SpawnBlockingFailed(..)` | `system.spawn_blocking_failed` | `is_panic` / `is_cancelled` |
+//!
+//! ## `MapleCacheError` — `maple_cache.*`
+//!
+//! | Variant | Code | `details` fields |
+//! | -------------------------------- | ------------------------------------- | --------------------------------- |
+//! | `PathEmpty` | `maple_cache.path_empty` | — |
+//! | `PathNoParent { path }` | `maple_cache.path_no_parent` | `path` |
+//! | `PathNotFound { path }` | `maple_cache.path_not_found` | `path` |
+//! | `PathNotADir { path }` | `maple_cache.path_not_a_dir` | `path` |
+//! | `ReadDirFailed { path, source }` | `maple_cache.read_dir_failed` | `path` / `io_kind` |
+//! | `SpawnBlockingFailed(..)` | `maple_cache.spawn_blocking_failed` | `is_panic` / `is_cancelled` |
+//!
+//! ## Command-layer `system.*` codes (no domain counterpart)
+//!
+//! Unlike the eight tables above, these codes are minted inside the
+//! command / boot layer for failures that have no `services/*`
+//! counterpart (boot-time resource resolution, ad-hoc `tokio` task
+//! plumbing in [`super::system::ping`]). They share the `system.*`
+//! namespace with [`SystemError`] so the `code` column stays
+//! globally searchable; [`super::system::ping`] in particular
+//! re-uses the `system.spawn_blocking_failed` code without going
+//! through [`SystemError`] (the smoke command pre-dates the service
+//! layer).
+//!
+//! | Code | Origin | `details` fields |
+//! | ------------------------------- | ----------------------------------------------------------------------------- | ---------------- |
+//! | `system.app_data_missing` | [`crate::run`] — `%APPDATA%` env var is unset or empty (Windows boot). | — |
+//! | `system.spawn_blocking_failed` | [`super::system::ping`] ad-hoc path — same code as [`SystemError::SpawnBlockingFailed`] but minted without constructing the service error. | — |
+//!
+//! ## Command-layer `launcher.*` codes (no domain counterpart)
+//!
+//! Minted inside [`super::launcher`] for orchestration failures
+//! that happen **outside** the [`GameError`] surface — i.e. setup
+//! steps the service layer doesn't reach. Every launch-time
+//! business failure (path validation, locale remulator release,
+//! ShellExecuteW, Command::spawn) still flows through the
+//! [`GameError`] / [`game.*`](#gameerror--commanderror-servicesgame)
+//! table; these command-only codes cover the edges.
+//!
+//! | Code | Origin | `details` fields |
+//! | ------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------- |
+//! | `launcher.target_dir_resolve_failed` | [`super::launcher::launch_game`] — [`default_target_dir`][dtd] returned `io::Error`. | `io_kind` |
+//! | `launcher.spawn_blocking_failed` | [`super::launcher::launch_game`] — [`tokio::task::JoinError`] from `spawn_blocking`. | `is_panic` / `is_cancelled` |
+//!
+//! [dtd]: crate::services::game::default_target_dir
+//!
+//! # Usage at the command boundary
+//!
+//! ```ignore
+//! # use beanfun_lib::commands::error::CommandError;
+//! # use beanfun_lib::services::beanfun::error::LoginError;
+//! #[tauri::command]
+//! async fn login() -> Result<(), CommandError> {
+//! // `?` converts the domain error through the `Into`
+//! // blanket impl produced by the `From` impls in this module.
+//! do_login().await?;
+//! Ok(())
+//! }
+//!
+//! # async fn do_login() -> Result<(), LoginError> { Err(LoginError::MissingSessionKey) }
+//! ```
+
+use std::io;
+
+use serde::Serialize;
+use serde_json::json;
+use specta::Type;
+
+use crate::services::beanfun::error::LoginError;
+use crate::services::config::error::ConfigError;
+use crate::services::game::error::GameError;
+use crate::services::maple_cache::error::MapleCacheError;
+use crate::services::process::error::ProcessError;
+use crate::services::registry::error::RegistryError;
+use crate::services::storage::aes_backup::BackupError;
+use crate::services::storage::error::StorageError;
+use crate::services::system::error::SystemError;
+use crate::services::updater::error::UpdaterError;
+
+/// IPC-facing error DTO. Preserves a stable `{ code, message, details }`
+/// shape across every Tauri command.
+///
+/// # Serialization contract
+///
+/// Always serialized as a JSON object with exactly three keys:
+/// `code` (string), `message` (string), `details` (nullable JSON value).
+/// Frontend types live in
+/// `src/types/bindings.ts` and are auto-generated by
+/// `tauri-specta` (P10 chunk 10.1 D8).
+///
+/// # Construction
+///
+/// Use [`CommandError::new`] for the minimum required pair and chain
+/// [`CommandError::with_details`] when the domain has extra structured
+/// context worth exposing to the UI.
+///
+/// # Display / Error
+///
+/// [`Display`][std::fmt::Display] formats as `[code] message` for
+/// `tracing` logs; the type also implements [`std::error::Error`] so
+/// it composes with existing `?` / `anyhow` call sites if a command
+/// needs to chain through additional fallible ops before surfacing.
+#[derive(Debug, Clone, Serialize, Type)]
+pub struct CommandError {
+ /// Stable `.` identifier — see
+ /// [module-level docs](self#code-naming) for the full mapping table.
+ pub code: String,
+ /// Human-readable, non-localized Rust-side description sourced from
+ /// the domain error's `Display` impl. Safe to
+ /// `tracing::error!(%err)`; never contains secrets — the domain
+ /// layer is responsible for redaction (see P8.2 R8.2-1
+ /// `LaunchRequest` as the template).
+ pub message: String,
+ /// Optional structured context — the domain may attach any
+ /// JSON-representable payload (flat objects preferred). The
+ /// frontend treats this field as `unknown`-shaped and branches on
+ /// `code` before reading fields.
+ pub details: Option,
+}
+
+impl CommandError {
+ /// Build a new [`CommandError`] with no `details`.
+ ///
+ /// `code` **must** follow the module-level naming convention.
+ pub fn new(code: impl Into, message: impl Into) -> Self {
+ Self {
+ code: code.into(),
+ message: message.into(),
+ details: None,
+ }
+ }
+
+ /// Attach (or overwrite) the `details` field, consuming the
+ /// builder. Accepts anything that serializes via
+ /// [`serde_json::to_value`]; serialization failure degrades
+ /// gracefully to `details = None` rather than losing the error —
+ /// the primary `{ code, message }` contract is preserved no matter
+ /// what.
+ pub fn with_details(mut self, details: T) -> Self {
+ self.details = serde_json::to_value(details).ok();
+ self
+ }
+}
+
+impl std::fmt::Display for CommandError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "[{}] {}", self.code, self.message)
+ }
+}
+
+impl std::error::Error for CommandError {}
+
+// ---------------------------------------------------------------------
+// Small helpers shared by multiple `From` impls. Kept private so the
+// encoding quirks (reqwest flag extraction, io_kind stringification)
+// stay quarantined to this module.
+// ---------------------------------------------------------------------
+
+/// Stringify an [`io::ErrorKind`] via its `Debug` impl.
+/// [`io::ErrorKind`] is `#[non_exhaustive]` and does not implement
+/// `Display` nor serde; its `Debug` form ("NotFound",
+/// "PermissionDenied", …) is stable enough for diagnostic display.
+fn io_kind_str(err: &io::Error) -> String {
+ format!("{:?}", err.kind())
+}
+
+/// Extract the standard reqwest signal flags into a flat JSON object
+/// consumable by the frontend. Used by every variant that wraps a
+/// [`reqwest::Error`] (LoginError::Http, UpdaterError::Probe/Fetch).
+fn reqwest_details(err: &reqwest::Error) -> serde_json::Value {
+ json!({
+ "is_timeout": err.is_timeout(),
+ "is_connect": err.is_connect(),
+ "is_request": err.is_request(),
+ "status": err.status().map(|s| s.as_u16()),
+ "url": err.url().map(|u| u.as_str()),
+ })
+}
+
+/// Extract line / column from a [`serde_json::Error`] (1-indexed, as
+/// reported by the crate). Position is `Some(0, 0)` for category
+/// mismatches where no offset is available — the frontend can branch
+/// on `0/0` if it cares.
+fn serde_json_details(err: &serde_json::Error) -> serde_json::Value {
+ json!({
+ "line": err.line(),
+ "column": err.column(),
+ })
+}
+
+// ---------------------------------------------------------------------
+// LoginError → CommandError (services/beanfun)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: LoginError) -> Self {
+ // Capture Display output *before* moving `e` into the match so
+ // every arm can reuse the same `message` string without
+ // rebuilding it per arm.
+ let message = e.to_string();
+ match e {
+ LoginError::MissingSessionKey => CommandError::new("auth.missing_session_key", message),
+ LoginError::EmptyResponse => CommandError::new("auth.empty_response", message),
+ LoginError::MissingVerificationToken => {
+ CommandError::new("auth.missing_verification_token", message)
+ }
+ LoginError::MissingViewState => CommandError::new("auth.missing_view_state", message),
+ LoginError::MissingViewStateGenerator => {
+ CommandError::new("auth.missing_view_state_generator", message)
+ }
+ LoginError::MissingEventValidation => {
+ CommandError::new("auth.missing_event_validation", message)
+ }
+ LoginError::AdvanceCheckRequired { url } => {
+ CommandError::new("auth.advance_check_required", message)
+ .with_details(json!({ "url": url }))
+ }
+ LoginError::TotpRequired(challenge) => {
+ // P10.1 keeps the payload compact (`challenge_display`
+ // only). P10.2 will upgrade `TotpChallenge` to
+ // `Serialize + specta::Type` and inline `viewstate` /
+ // `url` so `login_totp` can round-trip structured data.
+ CommandError::new("auth.totp_required", message)
+ .with_details(json!({ "challenge_display": format!("{challenge:?}") }))
+ }
+ LoginError::ServerMessage(text) => CommandError::new("auth.server_rejected", message)
+ .with_details(json!({ "server_message": text })),
+ LoginError::SendLoginNoFormData => {
+ CommandError::new("auth.send_login_no_form_data", message)
+ }
+ LoginError::MissingAkey => CommandError::new("auth.missing_akey", message),
+ LoginError::MissingWebToken => CommandError::new("auth.missing_web_token", message),
+ LoginError::QrInitResultError => {
+ CommandError::new("auth.qr_init_result_error", message)
+ }
+ LoginError::QrJsonParseFailed => {
+ CommandError::new("auth.qr_json_parse_failed", message)
+ }
+ LoginError::QrUnsupportedRegion => {
+ CommandError::new("auth.qr_unsupported_region", message)
+ }
+ LoginError::GamepassUnsupportedRegion => {
+ CommandError::new("auth.gamepass_unsupported_region", message)
+ }
+ LoginError::DeviceRegistrationRequired {
+ poll_url, param, ..
+ } => CommandError::new("auth.device_registration_required", message).with_details(
+ json!({
+ "poll_url": poll_url,
+ "param": param,
+ }),
+ ),
+ LoginError::DeviceLoginTimeout => {
+ CommandError::new("auth.device_login_timeout", message)
+ }
+ LoginError::DeviceLoginRejected => {
+ CommandError::new("auth.device_login_rejected", message)
+ }
+ LoginError::OtpMissingLongPollingKey { snippet } => {
+ CommandError::new("auth.otp_missing_long_polling_key", message)
+ .with_details(json!({ "snippet": snippet }))
+ }
+ LoginError::OtpMissingUnkData => {
+ CommandError::new("auth.otp_missing_unk_data", message)
+ }
+ LoginError::OtpMissingCreateTime => {
+ CommandError::new("auth.otp_missing_create_time", message)
+ }
+ LoginError::OtpMissingSecretCode => {
+ CommandError::new("auth.otp_missing_secret_code", message)
+ }
+ LoginError::OtpEmptyResponse => CommandError::new("auth.otp_empty_response", message),
+ LoginError::OtpServerRejected { message: server } => {
+ CommandError::new("auth.otp_server_rejected", message)
+ .with_details(json!({ "server_message": server }))
+ }
+ LoginError::OtpDecryptionFailed { cause } => {
+ CommandError::new("auth.otp_decryption_failed", message)
+ .with_details(json!({ "cause": cause }))
+ }
+ LoginError::VerifyMissingViewState => {
+ CommandError::new("auth.verify_missing_view_state", message)
+ }
+ LoginError::VerifyMissingEventValidation => {
+ CommandError::new("auth.verify_missing_event_validation", message)
+ }
+ LoginError::VerifyMissingSampleCaptcha => {
+ CommandError::new("auth.verify_missing_sample_captcha", message)
+ }
+ LoginError::VerifyMissingLblAuthType => {
+ CommandError::new("auth.verify_missing_lbl_auth_type", message)
+ }
+ LoginError::VerifyCaptchaImageTooSmall { actual } => {
+ CommandError::new("auth.verify_captcha_image_too_small", message)
+ .with_details(json!({ "actual_bytes": actual }))
+ }
+ LoginError::AccountMgmtMissingViewState => {
+ CommandError::new("auth.account_mgmt_missing_view_state", message)
+ }
+ LoginError::AccountMgmtMissingViewStateGenerator => {
+ CommandError::new("auth.account_mgmt_missing_view_state_generator", message)
+ }
+ LoginError::AccountMgmtMissingEventValidation => {
+ CommandError::new("auth.account_mgmt_missing_event_validation", message)
+ }
+ LoginError::AccountMgmtMissingGameName => {
+ CommandError::new("auth.account_mgmt_missing_game_name", message)
+ }
+ LoginError::AccountMgmtMissingAccountLen => {
+ CommandError::new("auth.account_mgmt_missing_account_len", message)
+ }
+ LoginError::GameListServiceListMissing => {
+ CommandError::new("game.service_list_missing", message)
+ }
+ LoginError::Http(err) => {
+ let details = reqwest_details(&err);
+ CommandError::new("network.http_failed", message).with_details(details)
+ }
+ LoginError::BodyTooLarge { limit, actual } => {
+ CommandError::new("network.body_too_large", message)
+ .with_details(json!({ "limit": limit, "actual": actual }))
+ }
+ LoginError::Json(err) => {
+ let details = serde_json_details(&err);
+ CommandError::new("network.json_decode_failed", message).with_details(details)
+ }
+ LoginError::Parser(err) => {
+ // `ParserError` is a small unit-variant enum — encode
+ // its variant name via `Debug` so the frontend can
+ // branch on "MissingViewState" / "MissingAkey" /
+ // "MissingRequestVerificationToken" without Specta
+ // needing to know the full type.
+ let variant = format!("{err:?}");
+ CommandError::new("auth.html_parse_failed", message)
+ .with_details(json!({ "parser_variant": variant }))
+ }
+ LoginError::InvalidUrl(url) => {
+ CommandError::new("auth.invalid_url", message).with_details(json!({ "url": url }))
+ }
+ LoginError::InvalidUtf8 => CommandError::new("auth.invalid_utf8", message),
+ LoginError::Unknown(detail) => {
+ CommandError::new("auth.unknown", message).with_details(json!({ "detail": detail }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// StorageError → CommandError (services/storage)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: StorageError) -> Self {
+ let message = e.to_string();
+ match e {
+ StorageError::Dpapi {
+ operation,
+ message: win32,
+ } => CommandError::new("storage.dpapi_failed", message)
+ .with_details(json!({ "operation": operation, "win32_message": win32 })),
+ StorageError::Registry(err) => {
+ let kind = io_kind_str(&err);
+ CommandError::new("storage.registry_io_failed", message)
+ .with_details(json!({ "io_kind": kind }))
+ }
+ StorageError::EntropyMissing => CommandError::new("storage.entropy_missing", message),
+ StorageError::EntropyShape => CommandError::new("storage.entropy_shape", message),
+ StorageError::Io(err) => {
+ let kind = io_kind_str(&err);
+ CommandError::new("storage.io_failed", message)
+ .with_details(json!({ "io_kind": kind }))
+ }
+ StorageError::AppDataMissing => CommandError::new("storage.app_data_missing", message),
+ StorageError::Json(err) => {
+ let details = serde_json_details(&err);
+ CommandError::new("storage.json_failed", message).with_details(details)
+ }
+ StorageError::LegacyDataDetected { raw_bytes } => {
+ // Intentionally omit `raw_bytes` from `details` — the
+ // legacy ciphertext can be large (hundreds of KB) and
+ // may carry sensitive remnants. Only the byte count is
+ // surfaced; the command layer retains the full buffer
+ // for the subsequent NRBF migration pass.
+ CommandError::new("storage.legacy_data_detected", message)
+ .with_details(json!({ "byte_count": raw_bytes.len() }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// BackupError → CommandError (services/storage/aes_backup)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: BackupError) -> Self {
+ let message = e.to_string();
+ match e {
+ BackupError::InvalidCiphertext(err) => {
+ CommandError::new("storage.aes_backup_invalid_ciphertext", message)
+ .with_details(json!({ "reason": err.to_string() }))
+ }
+ // No `details` — the inner `cipher::block_padding::UnpadError`
+ // is intentionally opaque (RustCrypto's design choice to
+ // resist padding-oracle leakage). Adding a synthetic reason
+ // here would re-introduce that signal.
+ BackupError::DecryptFailed => {
+ CommandError::new("storage.aes_backup_decrypt_failed", message)
+ }
+ BackupError::InvalidUtf8(err) => {
+ CommandError::new("storage.aes_backup_invalid_utf8", message)
+ .with_details(json!({ "reason": err.to_string() }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// ConfigError → CommandError (services/config)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: ConfigError) -> Self {
+ let message = e.to_string();
+ match e {
+ ConfigError::Io(err) => {
+ let kind = io_kind_str(&err);
+ CommandError::new("config.io_failed", message)
+ .with_details(json!({ "io_kind": kind }))
+ }
+ ConfigError::XmlParse(_) => CommandError::new("config.xml_parse_failed", message),
+ ConfigError::XmlWrite(err) => {
+ let kind = io_kind_str(&err);
+ CommandError::new("config.xml_write_failed", message)
+ .with_details(json!({ "io_kind": kind }))
+ }
+ ConfigError::AppDataMissing => CommandError::new("config.app_data_missing", message),
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// ProcessError → CommandError (services/process)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: ProcessError) -> Self {
+ let message = e.to_string();
+ match e {
+ ProcessError::WmiInit(_) => CommandError::new("process.wmi_init_failed", message),
+ ProcessError::WmiConnect(_) => CommandError::new("process.wmi_connect_failed", message),
+ ProcessError::WmiQuery { query, .. } => {
+ CommandError::new("process.wmi_query_failed", message)
+ .with_details(json!({ "query": query }))
+ }
+ ProcessError::OpenProcess { pid, .. } => {
+ CommandError::new("process.open_process_failed", message)
+ .with_details(json!({ "pid": pid }))
+ }
+ ProcessError::TerminateProcess { pid, .. } => {
+ CommandError::new("process.terminate_process_failed", message)
+ .with_details(json!({ "pid": pid }))
+ }
+ ProcessError::PostMessage { hwnd, .. } => {
+ CommandError::new("process.post_message_failed", message)
+ .with_details(json!({ "hwnd": hwnd }))
+ }
+ ProcessError::NonAscii { offset, ch } => {
+ CommandError::new("process.non_ascii", message)
+ .with_details(json!({ "offset": offset, "char": ch.to_string() }))
+ }
+ ProcessError::Win32Call { name, .. } => {
+ CommandError::new("process.win32_call_failed", message)
+ .with_details(json!({ "win32_function": name }))
+ }
+ ProcessError::WindowNotFound {
+ primary_class,
+ fallback_class,
+ } => CommandError::new("process.window_not_found", message).with_details(json!({
+ "primary_class": primary_class,
+ "fallback_class": fallback_class,
+ })),
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// RegistryError → CommandError (services/registry)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: RegistryError) -> Self {
+ let message = e.to_string();
+ match e {
+ RegistryError::OpenKey {
+ hive,
+ subkey,
+ source,
+ } => {
+ let kind = io_kind_str(&source);
+ CommandError::new("registry.open_key_failed", message).with_details(json!({
+ "hive": hive.display_name(),
+ "subkey": subkey,
+ "io_kind": kind,
+ }))
+ }
+ RegistryError::ReadValue {
+ hive,
+ subkey,
+ value_name,
+ source,
+ } => {
+ let kind = io_kind_str(&source);
+ CommandError::new("registry.read_value_failed", message).with_details(json!({
+ "hive": hive.display_name(),
+ "subkey": subkey,
+ "value_name": value_name,
+ "io_kind": kind,
+ }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// GameError → CommandError (services/game)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: GameError) -> Self {
+ let message = e.to_string();
+ match e {
+ GameError::PathEmpty => CommandError::new("game.path_empty", message),
+ GameError::PathNotFound { path } => CommandError::new("game.path_not_found", message)
+ .with_details(json!({ "path": path.display().to_string() })),
+ GameError::PathNonAscii {
+ path,
+ offending_char,
+ position,
+ } => CommandError::new("game.path_non_ascii", message).with_details(json!({
+ "path": path.display().to_string(),
+ "char": offending_char.to_string(),
+ "position": position,
+ })),
+ GameError::LocaleRemulatorRelease { name, .. } => {
+ CommandError::new("game.locale_remulator_release_failed", message)
+ .with_details(json!({ "resource": name }))
+ }
+ GameError::LocaleRemulatorSha256Mismatch { name } => {
+ CommandError::new("game.locale_remulator_sha256_mismatch", message)
+ .with_details(json!({ "resource": name }))
+ }
+ #[cfg(windows)]
+ GameError::ShellExecute { code, .. } => {
+ CommandError::new("game.shellexecute_failed", message)
+ .with_details(json!({ "shellexecute_code": code }))
+ }
+ GameError::Spawn(err) => {
+ let kind = io_kind_str(&err);
+ CommandError::new("game.spawn_failed", message)
+ .with_details(json!({ "io_kind": kind }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// UpdaterError → CommandError (services/updater)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: UpdaterError) -> Self {
+ let message = e.to_string();
+ match e {
+ UpdaterError::Probe(err) => {
+ let details = reqwest_details(&err);
+ CommandError::new("update.probe_failed", message).with_details(details)
+ }
+ UpdaterError::Fetch(err) => {
+ let details = reqwest_details(&err);
+ CommandError::new("update.fetch_failed", message).with_details(details)
+ }
+ UpdaterError::JsonDecode(err) => {
+ let details = serde_json_details(&err);
+ CommandError::new("update.json_decode_failed", message).with_details(details)
+ }
+ UpdaterError::BodyTooLarge { limit, actual } => {
+ CommandError::new("update.body_too_large", message)
+ .with_details(json!({ "limit": limit, "actual": actual }))
+ }
+ UpdaterError::UnsupportedTag(tag) => {
+ CommandError::new("update.unsupported_tag", message)
+ .with_details(json!({ "tag": tag }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// SystemError → CommandError (services/system — open_url, future
+// open_folder / reveal_in_finder)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: SystemError) -> Self {
+ let message = e.to_string();
+ match e {
+ SystemError::InvalidUrl { url, reason } => {
+ CommandError::new("system.invalid_url", message)
+ .with_details(json!({ "url": url, "reason": reason }))
+ }
+ SystemError::OpenFailed { url, source } => {
+ CommandError::new("system.open_url_failed", message).with_details(json!({
+ "url": url,
+ "io_kind": io_kind_str(&source),
+ }))
+ }
+ SystemError::SpawnBlockingFailed(join_err) => {
+ CommandError::new("system.spawn_blocking_failed", message).with_details(json!({
+ "is_panic": join_err.is_panic(),
+ "is_cancelled": join_err.is_cancelled(),
+ }))
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// MapleCacheError → CommandError (services/maple_cache — sweep
+// MapleStory's per-launch cache from the game install dir)
+// ---------------------------------------------------------------------
+
+impl From for CommandError {
+ fn from(e: MapleCacheError) -> Self {
+ let message = e.to_string();
+ match e {
+ MapleCacheError::PathEmpty => CommandError::new("maple_cache.path_empty", message),
+ MapleCacheError::PathNoParent { path } => {
+ CommandError::new("maple_cache.path_no_parent", message)
+ .with_details(json!({ "path": path }))
+ }
+ MapleCacheError::PathNotFound { path } => {
+ CommandError::new("maple_cache.path_not_found", message)
+ .with_details(json!({ "path": path.display().to_string() }))
+ }
+ MapleCacheError::PathNotADir { path } => {
+ CommandError::new("maple_cache.path_not_a_dir", message)
+ .with_details(json!({ "path": path.display().to_string() }))
+ }
+ MapleCacheError::ReadDirFailed { path, source } => {
+ let kind = io_kind_str(&source);
+ CommandError::new("maple_cache.read_dir_failed", message).with_details(json!({
+ "path": path.display().to_string(),
+ "io_kind": kind,
+ }))
+ }
+ MapleCacheError::SpawnBlockingFailed(join_err) => {
+ CommandError::new("maple_cache.spawn_blocking_failed", message).with_details(
+ json!({
+ "is_panic": join_err.is_panic(),
+ "is_cancelled": join_err.is_cancelled(),
+ }),
+ )
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // -----------------------------------------------------------------
+ // D3 — `CommandError` struct + builder (P10.1)
+ // -----------------------------------------------------------------
+
+ #[test]
+ fn new_sets_code_and_message_with_no_details() {
+ let err = CommandError::new("auth.invalid_credentials", "bad creds");
+ assert_eq!(err.code, "auth.invalid_credentials");
+ assert_eq!(err.message, "bad creds");
+ assert!(err.details.is_none());
+ }
+
+ #[test]
+ fn with_details_attaches_structured_context() {
+ let err = CommandError::new("network.timeout", "request timed out")
+ .with_details(json!({ "elapsed_ms": 30_000 }));
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("elapsed_ms")),
+ Some(&json!(30_000))
+ );
+ }
+
+ #[test]
+ fn with_details_accepts_arbitrary_serialize() {
+ #[derive(Serialize)]
+ struct Ctx {
+ pid: u32,
+ exit_code: i32,
+ }
+ let err = CommandError::new("process.kill_failed", "terminate failed").with_details(Ctx {
+ pid: 1234,
+ exit_code: -1,
+ });
+ let details = err.details.expect("details should be Some");
+ assert_eq!(details.get("pid"), Some(&json!(1234)));
+ assert_eq!(details.get("exit_code"), Some(&json!(-1)));
+ }
+
+ #[test]
+ fn display_is_bracketed_code_then_message() {
+ let err = CommandError::new("storage.io_failed", "disk full");
+ assert_eq!(format!("{err}"), "[storage.io_failed] disk full");
+ }
+
+ #[test]
+ fn implements_std_error_trait() {
+ fn assert_error(_: &E) {}
+ let err = CommandError::new("auth.invalid_credentials", "bad creds");
+ assert_error(&err);
+ }
+
+ #[test]
+ fn serializes_as_flat_json_with_three_keys() {
+ let err = CommandError::new("network.timeout", "timeout")
+ .with_details(json!({ "retry_after_s": 5 }));
+ let jv = serde_json::to_value(&err).expect("serializable");
+ assert_eq!(jv.get("code"), Some(&json!("network.timeout")));
+ assert_eq!(jv.get("message"), Some(&json!("timeout")));
+ assert_eq!(jv.get("details"), Some(&json!({ "retry_after_s": 5 })));
+ let obj = jv.as_object().expect("object");
+ assert_eq!(obj.len(), 3, "exactly three keys: code / message / details");
+ }
+
+ #[test]
+ fn serializes_details_as_null_when_absent() {
+ let err = CommandError::new("auth.logged_out", "session expired");
+ let jv = serde_json::to_value(&err).expect("serializable");
+ assert_eq!(jv.get("details"), Some(&json!(null)));
+ }
+}
+
+#[cfg(test)]
+mod from_impls_tests {
+ //! Representative coverage for the seven domain `From` impls.
+ //!
+ //! One to three cases per domain, prioritizing:
+ //!
+ //! - **Unit variants** (no fields) — verifies
+ //! `code` + `message` + `details = None` baseline.
+ //! - **Variants with structured fields** — verifies every field
+ //! appears in `details` under the documented name (the naming
+ //! table in the module-level doc is the source of truth).
+ //! - **Variants with transport types** (`io::Error`,
+ //! `serde_json::Error`) — verifies the helper fns
+ //! (`io_kind_str`, `serde_json_details`) plug in correctly.
+ //!
+ //! Variants wrapping external errors that are genuinely hard to
+ //! construct in tests (e.g. `reqwest::Error`, `windows::core::Error`,
+ //! `wmi::WMIError`) are deferred to the integration tier
+ //! (P10.1 D11 / future P10.2 HTTP smoke tests) — the code paths
+ //! are exercised by the same shared helpers already covered here
+ //! via `serde_json_details` / `io_kind_str`, so compile-time
+ //! coverage is complete.
+
+ use super::*;
+ use crate::services::beanfun::error::LoginError;
+ use crate::services::config::error::ConfigError;
+ use crate::services::game::error::GameError;
+ use crate::services::maple_cache::error::MapleCacheError;
+ use crate::services::process::error::ProcessError;
+ use crate::services::registry::error::RegistryError;
+ use crate::services::registry::Hive;
+ use crate::services::storage::aes_backup::BackupError;
+ use crate::services::storage::error::StorageError;
+ use crate::services::updater::error::UpdaterError;
+ use std::io;
+
+ // ----- LoginError ------------------------------------------------
+
+ #[test]
+ fn login_missing_session_key_has_no_details() {
+ let err: CommandError = LoginError::MissingSessionKey.into();
+ assert_eq!(err.code, "auth.missing_session_key");
+ assert!(err.details.is_none());
+ }
+
+ /// P12.1 D5a CP1 wire-string contract: the GamePass region
+ /// guard surfaces a dedicated typed code so the Vue layer can
+ /// localize it differently from the QR sibling. Pin both the
+ /// code and the absence of a `details` blob (no leaked region
+ /// echo / context — same shape as `qr_unsupported_region`).
+ #[test]
+ fn login_gamepass_unsupported_region_has_no_details() {
+ let err: CommandError = LoginError::GamepassUnsupportedRegion.into();
+ assert_eq!(err.code, "auth.gamepass_unsupported_region");
+ assert!(err.details.is_none());
+ }
+
+ #[test]
+ fn login_advance_check_required_carries_url() {
+ let err: CommandError = LoginError::AdvanceCheckRequired {
+ url: Some("https://example.com/advance".to_string()),
+ }
+ .into();
+ assert_eq!(err.code, "auth.advance_check_required");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("url")),
+ Some(&json!("https://example.com/advance"))
+ );
+ }
+
+ #[test]
+ fn login_advance_check_required_with_no_url_serializes_null() {
+ let err: CommandError = LoginError::AdvanceCheckRequired { url: None }.into();
+ assert_eq!(err.code, "auth.advance_check_required");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("url")),
+ Some(&json!(null))
+ );
+ }
+
+ #[test]
+ fn login_server_message_renames_to_server_message_field() {
+ let err: CommandError = LoginError::ServerMessage("帳號已被鎖定".into()).into();
+ assert_eq!(err.code, "auth.server_rejected");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("server_message")),
+ Some(&json!("帳號已被鎖定"))
+ );
+ }
+
+ #[test]
+ fn login_body_too_large_carries_limit_and_actual() {
+ let err: CommandError = LoginError::BodyTooLarge {
+ limit: 1024,
+ actual: 2048,
+ }
+ .into();
+ assert_eq!(err.code, "network.body_too_large");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("limit"), Some(&json!(1024)));
+ assert_eq!(details.get("actual"), Some(&json!(2048)));
+ }
+
+ #[test]
+ fn login_json_decode_plugs_into_serde_json_details() {
+ let serde_err = serde_json::from_str::("bad").unwrap_err();
+ let err: CommandError = LoginError::Json(serde_err).into();
+ assert_eq!(err.code, "network.json_decode_failed");
+ let details = err.details.expect("details present");
+ assert!(details.get("line").is_some(), "line must be set");
+ assert!(details.get("column").is_some(), "column must be set");
+ }
+
+ #[test]
+ fn login_otp_server_rejected_renames_message_to_server_message() {
+ let err: CommandError = LoginError::OtpServerRejected {
+ message: "OTP expired".into(),
+ }
+ .into();
+ assert_eq!(err.code, "auth.otp_server_rejected");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("server_message")),
+ Some(&json!("OTP expired"))
+ );
+ }
+
+ #[test]
+ fn device_registration_required_does_not_leak_login_token() {
+ let err: CommandError = LoginError::DeviceRegistrationRequired {
+ login_token: "SENSITIVE_TOKEN_VALUE".into(),
+ poll_url: "https://beanfun.com/poll".into(),
+ param: "some_param".into(),
+ }
+ .into();
+ assert_eq!(err.code, "auth.device_registration_required");
+ let details = err.details.expect("details present");
+ assert!(
+ details.get("login_token").is_none(),
+ "login_token must not leak to frontend: {details}",
+ );
+ assert_eq!(
+ details.get("poll_url"),
+ Some(&json!("https://beanfun.com/poll"))
+ );
+ assert_eq!(details.get("param"), Some(&json!("some_param")));
+ }
+
+ // ----- StorageError ----------------------------------------------
+
+ #[test]
+ fn storage_dpapi_carries_operation_and_win32_message() {
+ let err: CommandError = StorageError::Dpapi {
+ operation: "Protect",
+ message: "NTE_BAD_DATA".into(),
+ }
+ .into();
+ assert_eq!(err.code, "storage.dpapi_failed");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("operation"), Some(&json!("Protect")));
+ assert_eq!(details.get("win32_message"), Some(&json!("NTE_BAD_DATA")));
+ }
+
+ #[test]
+ fn storage_app_data_missing_has_no_details() {
+ let err: CommandError = StorageError::AppDataMissing.into();
+ assert_eq!(err.code, "storage.app_data_missing");
+ assert!(err.details.is_none());
+ }
+
+ #[test]
+ fn storage_legacy_data_detected_exposes_byte_count_without_raw_bytes() {
+ // raw_bytes must never leak to the frontend — the legacy blob
+ // may carry sensitive remnants and can be 100s of KB.
+ let err: CommandError = StorageError::LegacyDataDetected {
+ raw_bytes: vec![1_u8, 2, 3, 4, 5],
+ }
+ .into();
+ assert_eq!(err.code, "storage.legacy_data_detected");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("byte_count"), Some(&json!(5)));
+ assert!(
+ details.get("raw_bytes").is_none(),
+ "raw bytes must not be exposed to frontend"
+ );
+ }
+
+ #[test]
+ fn storage_io_failed_stringifies_io_kind() {
+ let err: CommandError = StorageError::Io(io::Error::from(io::ErrorKind::NotFound)).into();
+ assert_eq!(err.code, "storage.io_failed");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("io_kind")),
+ Some(&json!("NotFound"))
+ );
+ }
+
+ // ----- BackupError (storage.aes_backup_*) -------------------------
+
+ #[test]
+ fn backup_invalid_ciphertext_carries_reason_string() {
+ let decode_err =
+ base64::Engine::decode(&base64::engine::general_purpose::STANDARD, "not!base64@")
+ .unwrap_err();
+ let err: CommandError = BackupError::InvalidCiphertext(decode_err).into();
+ assert_eq!(err.code, "storage.aes_backup_invalid_ciphertext");
+ let details = err.details.expect("details present");
+ assert!(
+ details
+ .get("reason")
+ .and_then(|v| v.as_str())
+ .is_some_and(|s| !s.is_empty()),
+ "reason must be a non-empty string sourced from base64::DecodeError",
+ );
+ }
+
+ #[test]
+ fn backup_decrypt_failed_is_opaque() {
+ let err: CommandError = BackupError::DecryptFailed.into();
+ assert_eq!(err.code, "storage.aes_backup_decrypt_failed");
+ assert!(
+ err.details.is_none(),
+ "DecryptFailed must surface no details — opaque by design (padding-oracle leakage avoidance)",
+ );
+ }
+
+ #[test]
+ fn backup_invalid_utf8_carries_reason_string() {
+ let utf8_err = String::from_utf8(vec![0xff, 0xfe, 0xfd]).unwrap_err();
+ let err: CommandError = BackupError::InvalidUtf8(utf8_err).into();
+ assert_eq!(err.code, "storage.aes_backup_invalid_utf8");
+ let details = err.details.expect("details present");
+ assert!(
+ details
+ .get("reason")
+ .and_then(|v| v.as_str())
+ .is_some_and(|s| !s.is_empty()),
+ "reason must be a non-empty string sourced from FromUtf8Error",
+ );
+ }
+
+ // ----- ConfigError -----------------------------------------------
+
+ #[test]
+ fn config_io_failed_stringifies_io_kind() {
+ let err: CommandError =
+ ConfigError::Io(io::Error::from(io::ErrorKind::PermissionDenied)).into();
+ assert_eq!(err.code, "config.io_failed");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("io_kind")),
+ Some(&json!("PermissionDenied"))
+ );
+ }
+
+ #[test]
+ fn config_app_data_missing_has_no_details() {
+ let err: CommandError = ConfigError::AppDataMissing.into();
+ assert_eq!(err.code, "config.app_data_missing");
+ assert!(err.details.is_none());
+ }
+
+ // ----- ProcessError ----------------------------------------------
+
+ #[test]
+ fn process_non_ascii_carries_offset_and_char() {
+ let err: CommandError = ProcessError::NonAscii {
+ offset: 7,
+ ch: '中',
+ }
+ .into();
+ assert_eq!(err.code, "process.non_ascii");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("offset"), Some(&json!(7)));
+ assert_eq!(details.get("char"), Some(&json!("中")));
+ }
+
+ #[test]
+ fn process_window_not_found_carries_primary_and_fallback_classes() {
+ let err: CommandError = ProcessError::WindowNotFound {
+ primary_class: "MapleStoryClass".into(),
+ fallback_class: Some("MapleStoryClassTW".into()),
+ }
+ .into();
+ assert_eq!(err.code, "process.window_not_found");
+ let details = err.details.expect("details present");
+ assert_eq!(
+ details.get("primary_class"),
+ Some(&json!("MapleStoryClass"))
+ );
+ assert_eq!(
+ details.get("fallback_class"),
+ Some(&json!("MapleStoryClassTW"))
+ );
+ }
+
+ #[test]
+ fn process_window_not_found_serializes_null_fallback_when_absent() {
+ let err: CommandError = ProcessError::WindowNotFound {
+ primary_class: "NexonGameClass".into(),
+ fallback_class: None,
+ }
+ .into();
+ assert_eq!(err.code, "process.window_not_found");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("primary_class"), Some(&json!("NexonGameClass")));
+ assert_eq!(details.get("fallback_class"), Some(&json!(null)));
+ }
+
+ // ----- RegistryError ---------------------------------------------
+
+ #[test]
+ fn registry_open_key_carries_hive_display_name_and_io_kind() {
+ let err: CommandError = RegistryError::OpenKey {
+ hive: Hive::CurrentUser,
+ subkey: r"Software\Beanfun".into(),
+ source: io::Error::from(io::ErrorKind::NotFound),
+ }
+ .into();
+ assert_eq!(err.code, "registry.open_key_failed");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("hive"), Some(&json!("HKEY_CURRENT_USER")));
+ assert_eq!(details.get("subkey"), Some(&json!(r"Software\Beanfun")));
+ assert_eq!(details.get("io_kind"), Some(&json!("NotFound")));
+ }
+
+ // ----- GameError -------------------------------------------------
+
+ #[test]
+ fn game_path_empty_has_no_details() {
+ let err: CommandError = GameError::PathEmpty.into();
+ assert_eq!(err.code, "game.path_empty");
+ assert!(err.details.is_none());
+ }
+
+ #[test]
+ fn game_path_not_found_carries_stringified_path() {
+ let err: CommandError = GameError::PathNotFound {
+ path: std::path::PathBuf::from(r"C:\Games\missing.exe"),
+ }
+ .into();
+ assert_eq!(err.code, "game.path_not_found");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("path"), Some(&json!(r"C:\Games\missing.exe")));
+ }
+
+ // ----- MapleCacheError -------------------------------------------
+
+ #[test]
+ fn maple_cache_path_empty_has_no_details() {
+ let err: CommandError = MapleCacheError::PathEmpty.into();
+ assert_eq!(err.code, "maple_cache.path_empty");
+ assert!(err.details.is_none());
+ }
+
+ #[test]
+ fn maple_cache_path_not_found_carries_stringified_path() {
+ let err: CommandError = MapleCacheError::PathNotFound {
+ path: std::path::PathBuf::from(r"C:\Games\MapleStory"),
+ }
+ .into();
+ assert_eq!(err.code, "maple_cache.path_not_found");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("path"), Some(&json!(r"C:\Games\MapleStory")));
+ }
+
+ #[test]
+ fn maple_cache_read_dir_failed_stringifies_io_kind_and_path() {
+ let err: CommandError = MapleCacheError::ReadDirFailed {
+ path: std::path::PathBuf::from(r"C:\Games\MapleStory"),
+ source: io::Error::from(io::ErrorKind::PermissionDenied),
+ }
+ .into();
+ assert_eq!(err.code, "maple_cache.read_dir_failed");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("path"), Some(&json!(r"C:\Games\MapleStory")));
+ assert_eq!(details.get("io_kind"), Some(&json!("PermissionDenied")));
+ }
+
+ // ----- UpdaterError ----------------------------------------------
+
+ #[test]
+ fn updater_body_too_large_carries_limit_and_actual() {
+ let err: CommandError = UpdaterError::BodyTooLarge {
+ limit: 5_242_880,
+ actual: 10_000_000,
+ }
+ .into();
+ assert_eq!(err.code, "update.body_too_large");
+ let details = err.details.expect("details present");
+ assert_eq!(details.get("limit"), Some(&json!(5_242_880)));
+ assert_eq!(details.get("actual"), Some(&json!(10_000_000)));
+ }
+
+ #[test]
+ fn updater_unsupported_tag_carries_tag() {
+ let err: CommandError = UpdaterError::UnsupportedTag("v100".into()).into();
+ assert_eq!(err.code, "update.unsupported_tag");
+ assert_eq!(
+ err.details.as_ref().and_then(|v| v.get("tag")),
+ Some(&json!("v100"))
+ );
+ }
+}
diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs
new file mode 100644
index 0000000..565916e
--- /dev/null
+++ b/src-tauri/src/commands/game.rs
@@ -0,0 +1,88 @@
+//! Game commands — fetch the per-region game catalogue (INI +
+//! ServiceList) so the frontend's `GameList.vue` dialog and the
+//! `selectedGameChanged` pipeline have something to render.
+//!
+//! # Family exposed in P12.3
+//!
+//! | Command | Family | Purpose |
+//! |---------------|--------|--------------------------------------------------------------------------|
+//! | [`list_games`]| game | Atomic INI + ServiceList fetch for the current session's region |
+//!
+//! # Why one command instead of two?
+//!
+//! WPF's `MainWindow.xaml.cs::reLoadGameInfo` (L682-729) fetches
+//! `get_service_ini.ashx` and `game_zone/` back-to-back inside one
+//! method body and only writes the parsed result into
+//! `MainWindow.GameList[region]` once **both** halves have parsed
+//! successfully. Splitting them across two IPC calls would either
+//! leave the frontend with a half-populated `useGameStore` mid-load
+//! (race-y for the GameList dialog open/close cycle) or force the
+//! frontend to re-implement the atomicity guarantee. One command,
+//! one round-trip, one [`GameInfoBundle`] — matches WPF and
+//! keeps the IPC surface minimal (P10 SRP).
+//!
+//! # Why session-gated?
+//!
+//! WPF's only call sites for `reLoadGameInfo` are:
+//!
+//! - `loginCompleted()` (post-login bootstrap)
+//! - `selectedGameChanged()` (when `INIData == null`, i.e. the
+//! first per-session game switch)
+//!
+//! Both happen **after** the user has authenticated. The
+//! `get_service_ini.ashx` and `game_zone/` endpoints don't require
+//! the bfWebToken cookie technically, but gating on session here
+//! mirrors WPF's runtime invariant (game catalog is only ever
+//! requested from inside the logged-in shell) and keeps the
+//! [`State`] usage uniform across the command surface
+//! (every read goes through [`require_auth`]). If a future
+//! "splash screen browse without login" UX is wanted, a
+//! `list_games_anonymous(region)` sibling can be added without
+//! touching this one — locking down the session-gated variant
+//! today doesn't paint us into a corner.
+
+use tauri::State;
+
+use crate::commands::{error::CommandError, session::require_auth, state::AppState};
+use crate::services::beanfun::{list_games as service_list_games, GameInfoBundle};
+
+/// Fetch the per-region INI of executable metadata + the ordered
+/// list of game services for the active session's region.
+///
+/// Mirrors `MainWindow.xaml.cs::reLoadGameInfo` (L682-729) — one
+/// atomic fetch returns both halves so the frontend never observes
+/// a half-populated state.
+///
+/// # Returns
+///
+/// A [`GameInfoBundle`] with:
+///
+/// - `ini` — `HashMap` keyed by
+/// `_` (e.g. `"610074_T9"`).
+/// - `services` — `Vec` preserving server ordering.
+///
+/// # Errors
+///
+/// - `auth.session_required` — no login is active.
+/// - Every [`LoginError`][le] surfaced by the underlying service —
+/// `network.http_failed`, `network.body_too_large`,
+/// `network.json_decode_failed`, `auth.invalid_utf8`,
+/// `auth.invalid_url`, `auth.unknown` (non-2xx),
+/// `game.service_list_missing` (catastrophic upstream
+/// regression — `Services.ServiceList = …;` literal absent).
+///
+/// # Frontend usage
+///
+/// Called once per session by `useGameStore.loadGames()` on
+/// AccountList mount, with optional `force=true` re-runs if the
+/// user manually retries from the GameList dialog's error
+/// banner.
+///
+/// [le]: crate::services::beanfun::LoginError
+#[tauri::command]
+#[specta::specta]
+pub async fn list_games(state: State<'_, AppState>) -> Result {
+ let (client, _session) = require_auth(state.inner()).await?;
+ let bundle = service_list_games(&client).await?;
+ Ok(bundle)
+}
diff --git a/src-tauri/src/commands/launcher.rs b/src-tauri/src/commands/launcher.rs
new file mode 100644
index 0000000..b1bec91
--- /dev/null
+++ b/src-tauri/src/commands/launcher.rs
@@ -0,0 +1,1634 @@
+//! Game-launch Tauri commands — the thin async boundary between
+//! the frontend's "Run Game" button and the service layer's
+//! `launch_game` orchestrator.
+//!
+//! Ports the WPF `btn_Run_Game_Click` pipeline
+//! (`Beanfun/MainWindow.xaml.cs` L1727-1900) and its neighbouring
+//! game-path / process-management helpers to IPC. The split into
+//! **six** separate commands follows the P10.3 Q4=A (split
+//! list/kill) + Q6=B (detect = read+write one-shot) + Q8=A (D5a
+//! first, risk last) decisions in `Todo.md`.
+//!
+//! # Chunk layout
+//!
+//! | D-step | Command(s) | Status |
+//! | ------ | ------------------------------------------------------ | --------------- |
+//! | D5a | [`launch_game`] | **this module** |
+//! | D5b | [`set_game_path`] / [`detect_game_path`] | **this module** |
+//! | D5c | [`list_game_processes`] / [`kill_game_processes`] | **this module** |
+//! | D5d | [`auto_paste`] | **this module** |
+//!
+//! # D5a — `launch_game`
+//!
+//! The command is intentionally **thin**: it takes already-resolved
+//! ingredients (`game_path`, `mode`, `command_line_template`,
+//! `account`, `password`) from the frontend, assembles a
+//! [`LaunchRequest`], and hands it to the pre-existing
+//! [`services::game::launch_game`][svc] orchestrator under
+//! [`tokio::task::spawn_blocking`]. All business logic —
+//! path validation, locale-aware mode resolution, Normal /
+//! LocaleRemulator dispatch, SHA-256 integrity checks on LR
+//! resources, `ShellExecuteW` with `runas` verb — lives in the
+//! service layer and is covered by the chunk 8.1 / 8.2 test suite.
+//!
+//! Config I/O (reading `Path.`, `startGameMode`, per-game
+//! `CommandLine` template) is deliberately **not** done here —
+//! those round-trips belong to [`commands::config`][cfg] (D2) and
+//! the per-game INI pipeline (P11/P12). Keeping launch + config as
+//! separate Tauri invocations preserves SRP and matches the "one
+//! user-meaningful action per command" convention the rest of the
+//! P10 command layer follows.
+//!
+//! # Credentials handling (P10.3 Q7=A)
+//!
+//! `account` and `password` cross IPC as plaintext `String`
+//! parameters, matching the legacy WPF flow where
+//! `MainWindow.account` / `MainWindow.password` are in-memory
+//! strings that the launcher reads directly. The service-layer
+//! [`LaunchRequest`] has a bespoke [`Debug`][std::fmt::Debug]
+//! impl that redacts [`command_line`][lr::command_line] (post-
+//! substitution) to prevent accidental leakage through
+//! `tracing::debug!("{req:?}")`; this command inherits that
+//! guarantee by using `LaunchRequest` verbatim.
+//!
+//! The [`build_command_line`] helper short-circuits to `""` when
+//! **any** of `template` / `account` / `password` is empty, matching
+//! the WPF guard at `MainWindow.xaml.cs` L1867-1879
+//! (`account != null && password != null && account != ""
+//! && password != "" && game_commandLine != ""`). This means
+//! unauthenticated launches (games that don't accept CLI
+//! credentials) work by passing empty strings from the frontend.
+//!
+//! # Blocking isolation
+//!
+//! [`services::game::launch_game`][svc] is a synchronous function
+//! that ultimately calls either [`std::process::Command::spawn`]
+//! (Normal mode) or `ShellExecuteW` via the `windows` crate (LR
+//! mode on Windows). Both are blocking system calls that would
+//! stall the `tokio` async runtime if called inline.
+//! [`tokio::task::spawn_blocking`] offloads the whole orchestrator
+//! onto the blocking thread pool (P10-Q5 = A). Granularity is the
+//! **entire orchestrator**, not individual Win32 call sites — path
+//! validation + LR resource release + ShellExecute together run
+//! under one task so the async boundary has exactly one await
+//! point (easier for future tracing spans, no intermediate
+//! `Result` gymnastics).
+//!
+//! # Command-layer error codes
+//!
+//! See [`crate::commands::error`] for the full table. D5a / D5b
+//! introduce three **command-only** codes (no service-layer
+//! counterpart) for failures that happen in this module's
+//! orchestration:
+//!
+//! | Code | Origin |
+//! | --------------------------------------- | -------------------------------------------------------------------------------------------------- |
+//! | `launcher.target_dir_resolve_failed` | [`default_target_dir`] returned an `io::Error` (current_exe / parent resolution failed). |
+//! | `launcher.spawn_blocking_failed` | [`tokio::task::JoinError`] from a `spawn_blocking` call (task panicked or was aborted). |
+//! | `launcher.platform_unsupported` | [`detect_game_path`] called on a non-Windows build (registry access is HKCU-only, Windows-specific). |
+//!
+//! Every `services::game::launch_game` result flows through the
+//! existing [`From for CommandError`][gfrom] in
+//! [`crate::commands::error`], so `game.path_empty` /
+//! `game.path_not_found` / `game.shellexecute_failed` etc. surface
+//! unchanged without a second mapping layer (DRY).
+//!
+//! # D5b — `set_game_path` / `detect_game_path`
+//!
+//! Port of the WPF `selectedGameChanged` L574-607 branch that seeds
+//! `Config.xml` with a game's executable directory. Two complementary
+//! commands cover the user-meaningful halves:
+//!
+//! - [`set_game_path`] — store the user-chosen path for the given
+//! game code. Thin wrapper over
+//! [`crate::services::config::set_value`] with a standardised
+//! key format (see [`game_path_config_key`]). Cross-platform —
+//! the write side is just `Config.xml` I/O.
+//! - [`detect_game_path`] — check `Config.xml` first, then fall
+//! back to a registry lookup (HKCU, with the WPF-compatible
+//! `HKEY_LOCAL_MACHINE\` prefix strip on `dir_reg`), writing the
+//! discovered value back to `Config.xml` for future launches. This
+//! matches the P10.3 Q6 = B decision (read + write fused into one
+//! IPC call, matching WPF parity).
+//!
+//! ## Input shape (INI separation)
+//!
+//! Both commands take `dir_value_name` / `dir_reg` / `game_code` as
+//! explicit parameters instead of reading them from a per-game INI.
+//! The INI pipeline is a P11 concern (Vue frontend side) — keeping
+//! launcher commands INI-agnostic means:
+//!
+//! 1. **SRP** — one command, one side effect. No hidden "also reads
+//! `MapleStory_TW.ini` to look up registry hive".
+//! 2. **Testability** — unit tests can exercise the detect flow
+//! against synthetic `dir_reg` / `dir_value_name` without
+//! provisioning an INI.
+//! 3. **Forward compat** — when P11 introduces a `read_game_ini`
+//! command the frontend can compose it with these calls without
+//! this module carrying the dependency.
+//!
+//! ## Config key format
+//!
+//! WPF uses `{dir_value_name}.{game_code}` (L575 / L590 / L604) —
+//! e.g. `ExecPath.610074_T9`. [`game_path_config_key`] encapsulates
+//! that format so neither side of the IPC boundary has to re-derive
+//! it.
+//!
+//! ## `detect_game_path` body flow
+//!
+//! ```text
+//! 1. key = game_path_config_key(dir_value_name, game_code)
+//! 2. let cached = Config[key]
+//! if cached != "" → return Some(cached) (no registry call)
+//! 3. if dir_reg == "" → return None (WPF L578 guard)
+//! 4. subkey = dir_reg.strip_prefix("HKEY_LOCAL_MACHINE\\").unwrap_or(dir_reg)
+//! (WPF L580 literal strip)
+//! 5. spawn_blocking {
+//! registry::read_game_path(Hive::CurrentUser, subkey, dir_value_name)
+//! }
+//! 6. if found → Config[key] = value (WPF L589-592)
+//! 7. return the registry value (Some / None)
+//! ```
+//!
+//! Registry access is gated on `target_os = "windows"`; non-Windows
+//! builds return [`launcher.platform_unsupported`] via
+//! [`PLATFORM_UNSUPPORTED_CODE`]. [`set_game_path`] stays
+//! unconditional — Config I/O is portable.
+//!
+//! ## Blocking isolation (detect_game_path)
+//!
+//! Unlike [`launch_game`] (whole orchestrator under one
+//! `spawn_blocking`), [`detect_game_path`] keeps Config I/O on the
+//! tokio runtime (it's natively `async`) and only wraps the
+//! `winreg` call — the single synchronous island in the pipeline.
+//! This is a finer-grained split than D5a's "one big blocking box"
+//! rule because here the non-blocking parts genuinely exist: an
+//! `async` Config read that resolves in memory, a synchronous
+//! registry hit, and another `async` Config write. Three awaits is
+//! clearer than one `spawn_blocking` wrapping all of it.
+//!
+//! # D5c — `list_game_processes` / `kill_game_processes`
+//!
+//! Ports the "is the game already running?" preflight block of the
+//! WPF `btn_Run_Game_Click` flow (`MainWindow.xaml.cs` L1765-1833)
+//! to IPC. WPF does list-then-confirm-then-kill inline; we split
+//! that into two commands so the user-facing confirmation dialog
+//! stays on the Vue side (P10.3 Q4 = A, stateless pair):
+//!
+//! - [`list_game_processes`] — enumerate every running process
+//! whose executable path byte-equals `game_path`. Returns a
+//! [`Vec`][GameProcessInfo] so the UI can render
+//! "2 instances of MapleStory.exe are running" with the
+//! matching exe paths.
+//! - [`kill_game_processes`] — best-effort terminate the pids the
+//! frontend passes in. Returns the subset that actually died so
+//! the UI can re-check / re-render leftovers. **Does not**
+//! re-validate the pids against any game path — the design
+//! (P10.3 Q4 = A) puts the trust boundary at the frontend: it
+//! calls [`list_game_processes`] first, shows the confirm dialog,
+//! and only then forwards the resulting pids.
+//!
+//! ## IPC DTO vs service-layer type
+//!
+//! Service-layer [`crate::services::process::ProcessInfo`] is
+//! Windows-only (the whole `services::process` module is
+//! `#[cfg(target_os = "windows")]`). To keep the command signature
+//! cross-platform — a hard requirement from the P10 chunk layout
+//! so `bindings.ts` stays stable on dev boxes that `cargo check`
+//! on macOS / Linux — we surface [`GameProcessInfo`], a
+//! cross-platform DTO shaped as:
+//!
+//! ```text
+//! { pid: u32, name: String, executable_path: Option }
+//! ```
+//!
+//! `executable_path` is `Option` (rather than `PathBuf`) to
+//! avoid leaking the specta `PathBuf` quirks to the frontend and to
+//! let the UI treat missing paths uniformly. The conversion uses
+//! [`std::path::Path::to_string_lossy`] — in practice every game
+//! install path is ASCII so this is lossless; the docstring on
+//! [`GameProcessInfo::executable_path`] spells that out for
+//! pathological inputs.
+//!
+//! ## Blocking isolation (D5c)
+//!
+//! Both commands wrap their service-layer primitives in
+//! [`tokio::task::spawn_blocking`]:
+//!
+//! - [`list_game_processes`] → `find_game_processes` (WMI query)
+//! - [`kill_game_processes`] → `kill_game_processes` service
+//! (per-pid `OpenProcess` + `TerminateProcess`)
+//!
+//! Both primitives are synchronous Win32 / WMI calls — letting
+//! them run inline would block the `current_thread` runtime flavor
+//! (forbidden) and starve peers on the multi-threaded flavor.
+//!
+//! ## No new error codes (D5c)
+//!
+//! Every failure surfaces through existing mappings:
+//!
+//! - `process.wmi_init_failed` / `process.wmi_connect_failed` /
+//! `process.wmi_query_failed` / `process.open_process_failed` /
+//! `process.terminate_process_failed` — from the existing
+//! [`From for CommandError`][pfrom] conversion.
+//! - [`SPAWN_BLOCKING_FAILED_CODE`] — reused from D5a/D5b for
+//! Tokio `JoinError`.
+//! - [`PLATFORM_UNSUPPORTED_CODE`] — reused from D5b for non-
+//! Windows builds. Both new commands `#[cfg]`-gate their bodies
+//! and fall through to the same error shape.
+//!
+//! # D5d — `auto_paste`
+//!
+//! Ports the credential hand-off at the tail of `getOtpWorker_RunWorkerCompleted`
+//! (`MainWindow.xaml.cs` L2158-2238) to IPC. WPF fires this after
+//! `services/beanfun` resolves the OTP for the selected account —
+//! the frontend now owns the OTP string (the `check_otp` / `get_otp`
+//! commands return it), so the command layer's responsibility is
+//! just the Win32 sequence: find the launcher window, optionally
+//! click through the SEA pre-login prompt, clear the account /
+//! password fields, type the credentials, and submit.
+//!
+//! The orchestration itself lives in
+//! [`crate::services::process::auto_paste::paste_credentials`]
+//! (framework-agnostic, unit-tested against a recording
+//! [`PasteDriver`][pd] mock). This command is the thin IPC wrapper.
+//!
+//! ## IPC DTO shape
+//!
+//! [`AutoPasteRequest`] groups the four parameters into one struct
+//! (rather than four positional args) because:
+//!
+//! 1. **Readability** — call sites spell each field by name
+//! (`{ className, account, password, specialClick }`), so the
+//! frontend can't silently swap `account` and `password` in
+//! a refactor.
+//! 2. **Specta friendliness** — generates a `AutoPasteRequest`
+//! TypeScript interface the Vue side can type against,
+//! instead of a positional tuple.
+//! 3. **Future-proofing** — if WPF's hard-coded timings (100 ms /
+//! 100 ms / 200 ms) ever need to become runtime-tunable,
+//! adding a `Duration` field to one struct is cheaper than
+//! a breaking-change to the command signature.
+//!
+//! ## `specialClick` dispatch (P10.3 Q2 decision)
+//!
+//! The service layer takes a single `bool` rather than the
+//! `(service_code, service_region)` pair WPF tests (`== "610074"`
+//! and `== "T9"`, L2195). The command layer stays agnostic about
+//! "what counts as MapleStory SEA" — the frontend computes the
+//! boolean from the selected game and forwards it here. Keeps
+//! the Rust side free of MapleStory business rules that might
+//! churn with future game additions.
+//!
+//! ## Blocking isolation (D5d)
+//!
+//! `paste_credentials` is synchronous end-to-end (Win32 FFI +
+//! ~400 ms of `std::thread::sleep`). The command wraps the whole
+//! orchestration in one [`tokio::task::spawn_blocking`] — same
+//! granularity as D5a's "whole orchestrator" rule. The sleeps are
+//! deliberately `thread::sleep` (not `tokio::time::sleep`) inside
+//! the service layer because every step around them is already
+//! sync FFI; crossing back into async just to sleep would force a
+//! second `spawn_blocking` per step (see
+//! [`auto_paste` module docs][am] Q4 for the full reasoning).
+//!
+//! ## Credentials handling (D5d inherits P10.3 Q7=A)
+//!
+//! `account` and `password` cross IPC as plaintext, identical to
+//! [`launch_game`]. The D5d-specific risk: the password field is
+//! typically the freshly-issued OTP (rotates every ~30 s),
+//! narrowing the plaintext exposure window compared to launch_game's
+//! long-lived account password. No extra redaction is added — the
+//! frontend is expected to clear its OTP display state after the
+//! paste completes.
+//!
+//! ## No new error codes (D5d)
+//!
+//! Every failure routes through existing mappings:
+//!
+//! - `process.window_not_found` — **new in D5d** at the service layer
+//! (`ProcessError::WindowNotFound`), surfaced through the existing
+//! [`From for CommandError`][pfrom] conversion.
+//! Frontend branches on this code to fall back to clipboard-copy
+//! (mirrors WPF L2169-2174).
+//! - `process.post_message_failed` / `process.win32_call_failed` /
+//! `process.non_ascii` — existing conversions from other
+//! `services/process` modules.
+//! - [`SPAWN_BLOCKING_FAILED_CODE`] — reused for Tokio `JoinError`.
+//! - [`PLATFORM_UNSUPPORTED_CODE`] — reused for non-Windows builds.
+//!
+//! [pfrom]: crate::commands::error#processerror--commanderror-servicesprocess
+//! [lr::command_line]: crate::services::game::LaunchRequest::command_line
+//! [svc]: crate::services::game::launch_game
+//! [cfg]: crate::commands::config
+//! [gfrom]: crate::commands::error#gameerror--commanderror-servicesgame
+//! [`launcher.platform_unsupported`]: PLATFORM_UNSUPPORTED_CODE
+//! [pd]: crate::services::process::auto_paste::PasteDriver
+//! [am]: crate::services::process::auto_paste
+
+use std::path::PathBuf;
+
+use serde_json::json;
+use tauri::State;
+
+use crate::commands::config::config_xml_path;
+use crate::commands::error::CommandError;
+use crate::commands::state::AppState;
+use crate::services::config as svc_config;
+use crate::services::game::{
+ self, default_target_dir, substitute_credentials, GameStartMode, LaunchRequest,
+};
+
+/// IPC-shaped summary of a running game process, returned by
+/// [`list_game_processes`].
+///
+/// # Cross-platform availability
+///
+/// This type is defined at the command layer (not re-exported from
+/// [`crate::services::process`]) because the service-layer
+/// [`ProcessInfo`][svc_pi] lives inside a
+/// `#[cfg(target_os = "windows")]`-gated module. Surfacing the
+/// DTO here lets [`list_game_processes`] keep a cross-platform
+/// signature (the body errors out at runtime on non-Windows via
+/// [`PLATFORM_UNSUPPORTED_CODE`]) so `cargo check` on macOS /
+/// Linux dev boxes still produces a stable `bindings.ts`.
+///
+/// # Field semantics
+///
+/// | Field | Matches |
+/// | ----------------- | ---------------------------------------------------------- |
+/// | `pid` | `Win32_Process.ProcessId` (OS-level pid, stable for life) |
+/// | `name` | `Win32_Process.Name` (executable file name **with** ext) |
+/// | `executable_path` | `Win32_Process.ExecutablePath` — see **path encoding** below |
+///
+/// ## Path encoding
+///
+/// `executable_path: Option` is the UTF-8 form of the
+/// service-layer `Option`, produced via
+/// [`std::path::Path::to_string_lossy`]. Windows paths that land
+/// in `Win32_Process.ExecutablePath` are effectively always valid
+/// Unicode (the filesystem stores them as UTF-16 and WMI hands us
+/// the `String` form directly), so the `to_string_lossy` bridge
+/// is lossless in practice. `None` when WMI returned `NULL` (the
+/// process is protected or was mid-exit during enumeration).
+///
+/// [svc_pi]: crate::services::process::ProcessInfo
+#[derive(Debug, Clone, serde::Serialize, specta::Type)]
+#[serde(rename_all = "camelCase")]
+pub struct GameProcessInfo {
+ /// OS-level process id, stable for the process's lifetime.
+ pub pid: u32,
+
+ /// Executable file name **including** the `.exe` extension
+ /// (e.g. `"MapleStory.exe"`).
+ pub name: String,
+
+ /// UTF-8 path to the executable on disk, or `None` when WMI
+ /// couldn't read it (protected process or mid-exit). See the
+ /// struct-level "Path encoding" section for the conversion
+ /// rationale.
+ pub executable_path: Option,
+}
+
+/// IPC-shaped input for [`auto_paste`].
+///
+/// Groups the four per-call parameters (window class, account,
+/// password, SEA pre-click toggle) into one struct so the frontend
+/// spells each field by name — see the D5d section in the module
+/// docs for the rationale.
+///
+/// # Field semantics
+///
+/// | Field | WPF origin |
+/// | -------------- | ------------------------------------------------------------- |
+/// | `class_name` | `MainWindow.win_class_name` (L76, per-game INI column) |
+/// | `account` | `bfClient.accountList[index].sid` (L2149) |
+/// | `password` | `MainWindow.otp` (fresh OTP from `services/beanfun`, L2150) |
+/// | `special_click`| `"610074".Equals(service_code) && "T9".Equals(service_region)` (L2195) |
+///
+/// The fallback to `MapleStoryClassTW` (WPF L2161) is **hardcoded**
+/// inside [`crate::services::process::auto_paste`] — frontends
+/// that pass `className = "MapleStoryClass"` get the fallback for
+/// free; other class names go through without fallback (matches
+/// WPF's `"MapleStoryClass".Equals(win_class_name)` guard).
+#[derive(Debug, Clone, serde::Deserialize, specta::Type)]
+#[serde(rename_all = "camelCase")]
+pub struct AutoPasteRequest {
+ /// Top-level window class name of the launcher dialog
+ /// (e.g. `"MapleStoryClass"`, `"NexonGameClass"`). Sourced
+ /// from the per-game INI on the frontend side.
+ pub class_name: String,
+
+ /// Game account name to type into the login dialog. Must be
+ /// ASCII — non-ASCII bytes surface as `process.non_ascii`
+ /// (the existing Q3 contract from
+ /// [`crate::services::process::post_string::post_string`]).
+ pub account: String,
+
+ /// Password (or OTP) to type into the password field. Same
+ /// ASCII constraint as [`Self::account`].
+ pub password: String,
+
+ /// When `true`, inject the MapleStory-SEA pre-click sequence
+ /// (ESC + synthetic click at ~50% / 40% of the client area)
+ /// before typing credentials. WPF gates this on
+ /// `service_code == "610074" && service_region == "T9"` —
+ /// the command layer delegates the decision to the frontend
+ /// (see module docs).
+ pub special_click: bool,
+}
+
+/// Command-layer code minted when [`default_target_dir`] fails to
+/// resolve `current_exe().parent()`. Exposed as a `pub(crate)`
+/// const so [`crate::commands::error`] documentation tables and
+/// internal tests can pin the exact string without re-typing it.
+pub(crate) const TARGET_DIR_RESOLVE_FAILED_CODE: &str = "launcher.target_dir_resolve_failed";
+
+/// Command-layer code minted when [`tokio::task::spawn_blocking`]
+/// returns a [`tokio::task::JoinError`] (task panicked or was
+/// aborted). Sibling of [`TARGET_DIR_RESOLVE_FAILED_CODE`]; kept
+/// distinct from the [`crate::services::system::error::SystemError::SpawnBlockingFailed`]
+/// code so UI telemetry can tell "launcher path panicked" apart
+/// from "open_url path panicked" (P10.1 Q8.D4 fine-grained codes).
+pub(crate) const SPAWN_BLOCKING_FAILED_CODE: &str = "launcher.spawn_blocking_failed";
+
+/// Command-layer code returned by [`detect_game_path`] on
+/// non-Windows build targets. Registry access is HKCU-only and
+/// implemented via `winreg`, which is itself `#[cfg(windows)]`;
+/// dev boxes (macOS / Linux) can still `cargo check` the command
+/// signature — the body simply errors out at runtime.
+///
+/// Kept at module scope (rather than inlined into the non-Windows
+/// fallback helper) so the `platform_unsupported_code_is_stable`
+/// unit test can pin the exact string against rename drift — the
+/// frontend contract depends on this specific value. Mirrors the
+/// pattern established by [`crate::commands::storage`]'s
+/// `storage.platform_unsupported` code.
+#[cfg_attr(target_os = "windows", allow(dead_code))]
+pub(crate) const PLATFORM_UNSUPPORTED_CODE: &str = "launcher.platform_unsupported";
+
+#[cfg(not(target_os = "windows"))]
+fn platform_unsupported_error() -> CommandError {
+ CommandError::new(
+ PLATFORM_UNSUPPORTED_CODE,
+ "detect_game_path requires Windows (HKCU registry lookup for game install path)",
+ )
+}
+
+/// Format the `Config.xml` key for a game's executable directory.
+///
+/// WPF uses `{dir_value_name}.{game_code}` literally (see
+/// `MainWindow.xaml.cs` L575 / L590 / L604) — e.g.
+/// `ExecPath.610074_T9` for MapleStory TW. This helper is the
+/// **single point of truth** for the format so a refactor that
+/// accidentally flips the segment order (`"{game_code}.{dir_value_name}"`)
+/// or changes the separator is caught by the
+/// `game_path_config_key_format_is_dir_then_game` unit test rather
+/// than silently losing every user's saved paths on upgrade.
+///
+/// Both [`set_game_path`] and [`detect_game_path`] route through
+/// this helper (DRY) — the frontend never computes the key on its
+/// own, so neither WPF → Rust nor renderer → Rust boundaries can
+/// disagree on the format.
+pub(crate) fn game_path_config_key(dir_value_name: &str, game_code: &str) -> String {
+ format!("{dir_value_name}.{game_code}")
+}
+
+/// Build the `CreateProcess` / `ShellExecuteW` command-line string
+/// from a WPF-style template with `%s` placeholders.
+///
+/// Mirrors the WPF guard at `MainWindow.xaml.cs` L1867-1879: when
+/// any one of `template` / `account` / `password` is empty, the
+/// command line is entirely skipped (the game is launched without
+/// arguments). Otherwise, the first two `%s` placeholders are
+/// replaced with `account` and `password` via
+/// [`substitute_credentials`] — third-or-later `%s` are left
+/// literal, matching the two-pass `Regex.Replace(..., 1)` quirk in
+/// WPF.
+///
+/// Pulled out as a separate `pub(crate)` helper so the
+/// empty-string short-circuit logic is independently unit-testable
+/// (no `spawn_blocking` / `current_exe` dependencies) and kept
+/// DRY: future launcher commands that might want to echo the
+/// substituted command-line back to the UI (they shouldn't, due to
+/// the plaintext-password concern — see module docs) would reuse
+/// the same helper rather than re-deriving the guard.
+pub(crate) fn build_command_line(template: &str, account: &str, password: &str) -> String {
+ if template.is_empty() || account.is_empty() || password.is_empty() {
+ String::new()
+ } else {
+ substitute_credentials(template, account, password)
+ }
+}
+
+/// Launch the configured game binary with the current account
+/// credentials.
+///
+/// Thin wrapper over [`crate::services::game::launch_game`] — see the
+/// module-level docs for the full rationale, credential-handling
+/// policy, and blocking-isolation contract. The command performs
+/// three orchestration steps before delegating:
+///
+/// 1. Resolve the LocaleRemulator staging directory via
+/// [`default_target_dir`]. Fails with
+/// `launcher.target_dir_resolve_failed` if `current_exe()` or
+/// its `.parent()` is unavailable (extremely rare — only
+/// happens when the main binary has been deleted while
+/// running).
+/// 2. Assemble the command-line string via [`build_command_line`]
+/// (see that helper's docs for the empty-string short-circuit
+/// semantics).
+/// 3. Hand the [`LaunchRequest`] to the service orchestrator under
+/// [`tokio::task::spawn_blocking`]. A [`tokio::task::JoinError`]
+/// surfaces as `launcher.spawn_blocking_failed`; any
+/// [`crate::services::game::GameError`] from the orchestrator
+/// itself (path validation / LR resource release / ShellExecute
+/// / Command::spawn) flows through the existing
+/// [`From for CommandError`][gfrom] conversion.
+///
+/// # Parameters
+///
+/// - `game_path` — absolute path to the game executable (e.g.
+/// `C:\\Games\\MapleStory\\MapleStory.exe`). Frontend typically
+/// reads this from `Config.xml` via `get_config_value` — this
+/// command does not read Config itself (SRP).
+/// - `mode` — user's requested launch mode. `Auto` will resolve
+/// against the Windows system locale inside the service layer;
+/// see [`crate::services::game::resolve_mode`]. Maps to the
+/// legacy `startGameMode` integer config value on the frontend
+/// side.
+/// - `command_line_template` — per-game command-line template with
+/// `%s` placeholders. Empty string disables credential
+/// substitution entirely (the game is launched with no
+/// arguments). Typically sourced from the per-game INI pipeline
+/// that P11/P12 will implement.
+/// - `account` / `password` — the logged-in game account
+/// credentials. Both empty → no substitution (see
+/// [`build_command_line`]). Plaintext over IPC by P10.3 Q7=A
+/// decision; treat this command as sensitive at the callsite.
+///
+/// # Fire-and-forget
+///
+/// The spawned game process is detached — the service layer drops
+/// the `std::process::Child` immediately on Normal mode, and
+/// `ShellExecuteW` takes care of its own child on LR mode. There
+/// is no `pid` returned, no lifecycle tracking: matches the legacy
+/// WPF behaviour (P10.3 Q5 = A "stateless process commands").
+///
+/// [gfrom]: crate::commands::error#gameerror--commanderror-servicesgame
+#[tauri::command]
+#[specta::specta]
+pub async fn launch_game(
+ game_path: String,
+ mode: GameStartMode,
+ command_line_template: String,
+ account: String,
+ password: String,
+) -> Result<(), CommandError> {
+ let target_dir = default_target_dir().map_err(|err| {
+ CommandError::new(
+ TARGET_DIR_RESOLVE_FAILED_CODE,
+ format!("failed to resolve default target directory: {err}"),
+ )
+ .with_details(json!({ "io_kind": format!("{:?}", err.kind()) }))
+ })?;
+
+ let command_line = build_command_line(&command_line_template, &account, &password);
+
+ let req = LaunchRequest {
+ game_path: PathBuf::from(game_path),
+ command_line,
+ mode,
+ target_dir,
+ };
+
+ tokio::task::spawn_blocking(move || game::launch_game(&req))
+ .await
+ .map_err(|join_err| {
+ CommandError::new(
+ SPAWN_BLOCKING_FAILED_CODE,
+ format!("launch_game spawn_blocking failed: {join_err}"),
+ )
+ .with_details(json!({
+ "is_panic": join_err.is_panic(),
+ "is_cancelled": join_err.is_cancelled(),
+ }))
+ })??;
+
+ Ok(())
+}
+
+/// Persist the user-chosen game install path for `game_code` into
+/// `Config.xml`.
+///
+/// Thin wrapper over [`crate::services::config::set_value`] —
+/// see the D5b section in the module docs for the Config key format
+/// and the rationale for keeping `dir_value_name` / `game_code` as
+/// explicit parameters (INI separation).
+///
+/// # Parameters
+///
+/// - `game_code` — composite key the settings page supplies (e.g.
+/// `"610074_T9"`, from `service_code + "_" + service_region`).
+/// - `dir_value_name` — INI-sourced column name (e.g. `"ExecPath"`);
+/// becomes the prefix of the Config.xml key.
+/// - `path` — the chosen executable-dir path. Empty string is
+/// accepted and written verbatim; callers that want to *remove*
+/// the entry entirely should use
+/// [`crate::commands::config::set_config`] with `value = None`.
+///
+/// # Errors
+///
+/// - `config.io_failed` / `config.xml_write_failed` — see
+/// [`crate::services::config::ConfigError`] for the full surface.
+///
+/// # Platform
+///
+/// Unconditional — Config I/O is portable. Only
+/// [`detect_game_path`] requires Windows (registry lookup).
+#[tauri::command]
+#[specta::specta]
+pub async fn set_game_path(
+ state: State<'_, AppState>,
+ game_code: String,
+ dir_value_name: String,
+ path: String,
+) -> Result<(), CommandError> {
+ let config_path = config_xml_path(&state);
+ let key = game_path_config_key(&dir_value_name, &game_code);
+ svc_config::set_value(&config_path, &key, Some(&path)).await?;
+ Ok(())
+}
+
+/// Resolve the install path for `game_code`, consulting
+/// `Config.xml` first and falling back to the Windows registry.
+/// Writes any freshly-discovered registry value back to Config so
+/// future calls are fast (WPF parity — see L574-607).
+///
+/// Returns:
+/// - `Ok(Some(path))` — Config already had a value **or** the
+/// registry supplied one (in which case Config is now updated).
+/// - `Ok(None)` — both Config and the registry came up empty (or
+/// `dir_reg` was an empty string, meaning the INI has no fallback
+/// key configured). WPF shows an empty `t_GamePath` textbox in
+/// this case; this shape lets the frontend render the same way
+/// without another round-trip.
+///
+/// # Parameters
+///
+/// - `game_code` — composite identifier (`service_code_region`).
+/// - `dir_value_name` — INI-sourced Config column name and
+/// registry `REG_SZ` value name (WPF reuses the same string for
+/// both, L574 / L587).
+/// - `dir_reg` — INI-sourced registry subkey path. May contain a
+/// leading `HKEY_LOCAL_MACHINE\` literal which is stripped
+/// verbatim before the HKCU lookup (WPF L580 parity; see module
+/// docs for why only HKLM).
+///
+/// # Errors
+///
+/// - `config.io_failed` / `config.xml_write_failed` — the Config
+/// write-back step failed after a successful registry read.
+/// - `registry.open_key_failed` / `registry.read_value_failed` —
+/// the registry lookup surfaced a non-NotFound IO error (e.g.
+/// permission denied). NotFound / empty value / missing subkey
+/// are **not** errors — they fold into `Ok(None)` per WPF's
+/// silent fallback at L596-599.
+/// - `launcher.spawn_blocking_failed` — the registry-read
+/// `spawn_blocking` task panicked or was cancelled.
+/// - `launcher.platform_unsupported` — non-Windows build.
+#[tauri::command]
+#[specta::specta]
+pub async fn detect_game_path(
+ state: State<'_, AppState>,
+ game_code: String,
+ dir_value_name: String,
+ dir_reg: String,
+) -> Result