Compare commits
1 Commits
main
...
2aa1d5f92f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aa1d5f92f |
67
.github/workflows/release.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: 'publish'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- release
|
|
||||||
|
|
||||||
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-tauri:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
|
||||||
args: '--target aarch64-apple-darwin'
|
|
||||||
# - platform: 'macos-latest' # for Intel based macs.
|
|
||||||
# args: '--target x86_64-apple-darwin'
|
|
||||||
# - platform: 'ubuntu-22.04'
|
|
||||||
# args: ''
|
|
||||||
- platform: 'windows-latest'
|
|
||||||
args: ''
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: setup pnpm
|
|
||||||
uses: pnpm/action-setup@v5
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: setup node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
|
|
||||||
- name: install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
|
||||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
||||||
|
|
||||||
# - name: install dependencies (ubuntu only)
|
|
||||||
# if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: install frontend dependencies
|
|
||||||
run: pnpm install # change this to npm, pnpm or bun depending on which one you use.
|
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
with:
|
|
||||||
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
|
||||||
releaseName: 'v__VERSION__'
|
|
||||||
releaseBody: 'See the assets to download this version and install.'
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: false
|
|
||||||
args: ${{ matrix.args }}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "friendolls-desktop",
|
"name": "friendolls-desktop",
|
||||||
"version": "0.1.4",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-updater": "~2.10.0",
|
|
||||||
"tailwindcss": "^4.1.17"
|
"tailwindcss": "^4.1.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
@@ -17,9 +17,6 @@ importers:
|
|||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.5.2
|
version: 2.5.2
|
||||||
'@tauri-apps/plugin-updater':
|
|
||||||
specifier: ~2.10.0
|
|
||||||
version: 2.10.0
|
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
@@ -470,9 +467,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^5.2.0 || ^6 || ^7
|
vite: ^5.2.0 || ^6 || ^7
|
||||||
|
|
||||||
'@tauri-apps/api@2.10.1':
|
|
||||||
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
|
|
||||||
|
|
||||||
'@tauri-apps/api@2.9.0':
|
'@tauri-apps/api@2.9.0':
|
||||||
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
||||||
|
|
||||||
@@ -550,9 +544,6 @@ packages:
|
|||||||
'@tauri-apps/plugin-opener@2.5.2':
|
'@tauri-apps/plugin-opener@2.5.2':
|
||||||
resolution: {integrity: sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==}
|
resolution: {integrity: sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.0':
|
|
||||||
resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==}
|
|
||||||
|
|
||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
@@ -1146,8 +1137,6 @@ snapshots:
|
|||||||
tailwindcss: 4.1.17
|
tailwindcss: 4.1.17
|
||||||
vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
|
vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
|
||||||
|
|
||||||
'@tauri-apps/api@2.10.1': {}
|
|
||||||
|
|
||||||
'@tauri-apps/api@2.9.0': {}
|
'@tauri-apps/api@2.9.0': {}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.9.4':
|
'@tauri-apps/cli-darwin-arm64@2.9.4':
|
||||||
@@ -1201,10 +1190,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.9.0
|
'@tauri-apps/api': 2.9.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.10.0':
|
|
||||||
dependencies:
|
|
||||||
'@tauri-apps/api': 2.10.1
|
|
||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|||||||
1
src-tauri/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
API_BASE_URL=http://127.0.0.1:3000
|
||||||
255
src-tauri/Cargo.lock
generated
@@ -72,15 +72,6 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbitrary"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
|
||||||
dependencies = [
|
|
||||||
"derive_arbitrary",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -810,17 +801,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_arbitrary"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.110",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -1154,17 +1134,6 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "filetime"
|
|
||||||
version = "0.2.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"libredox",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1240,7 +1209,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "friendolls-desktop"
|
name = "friendolls-desktop"
|
||||||
version = "0.1.4"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"device_query",
|
"device_query",
|
||||||
@@ -1269,13 +1238,11 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-macros",
|
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-positioner",
|
"tauri-plugin-positioner",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-updater",
|
|
||||||
"tauri-specta",
|
"tauri-specta",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1529,10 +1496,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1542,11 +1507,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1878,7 +1841,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1949,9 +1911,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ico"
|
name = "ico"
|
||||||
version = "0.5.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png",
|
||||||
@@ -2339,7 +2301,6 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2369,12 +2330,6 @@ version = "0.4.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lua-src"
|
name = "lua-src"
|
||||||
version = "550.0.0"
|
version = "550.0.0"
|
||||||
@@ -2480,12 +2435,6 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minisign-verify"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -2890,18 +2839,6 @@ dependencies = [
|
|||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-osa-kit"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"objc2 0.6.3",
|
|
||||||
"objc2-app-kit",
|
|
||||||
"objc2-foundation 0.3.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3052,20 +2989,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "osakit"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
|
||||||
dependencies = [
|
|
||||||
"objc2 0.6.3",
|
|
||||||
"objc2-foundation 0.3.2",
|
|
||||||
"objc2-osa-kit",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -3457,61 +3380,6 @@ dependencies = [
|
|||||||
"memchr",
|
"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.17",
|
|
||||||
"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.2",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.42"
|
version = "1.0.42"
|
||||||
@@ -3750,8 +3618,6 @@ dependencies = [
|
|||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3759,7 +3625,6 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -3769,7 +3634,6 @@ dependencies = [
|
|||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3890,7 +3754,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -3903,7 +3766,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4642,17 +4504,6 @@ dependencies = [
|
|||||||
"syn 2.0.110",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tar"
|
|
||||||
version = "0.4.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
|
||||||
dependencies = [
|
|
||||||
"filetime",
|
|
||||||
"libc",
|
|
||||||
"xattr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4735,9 +4586,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.5.5"
|
version = "2.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29"
|
checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -4762,9 +4613,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.5.5"
|
version = "2.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7"
|
checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4893,38 +4744,6 @@ dependencies = [
|
|||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-updater"
|
|
||||||
version = "2.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"dirs",
|
|
||||||
"flate2",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"infer",
|
|
||||||
"log",
|
|
||||||
"minisign-verify",
|
|
||||||
"osakit",
|
|
||||||
"percent-encoding",
|
|
||||||
"reqwest",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tar",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror 2.0.17",
|
|
||||||
"time",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
"windows-sys 0.60.2",
|
|
||||||
"zip",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.9.1"
|
version = "2.9.1"
|
||||||
@@ -5007,9 +4826,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.8.3"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d"
|
checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -5168,21 +4987,6 @@ dependencies = [
|
|||||||
"zerovec",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.48.0"
|
version = "1.48.0"
|
||||||
@@ -5913,16 +5717,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -5967,15 +5761,6 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki-roots"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -6702,16 +6487,6 @@ version = "0.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xattr"
|
|
||||||
version = "1.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rustix",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xkbcommon"
|
name = "xkbcommon"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -6908,18 +6683,6 @@ dependencies = [
|
|||||||
"syn 2.0.110",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zip"
|
|
||||||
version = "4.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
|
||||||
dependencies = [
|
|
||||||
"arbitrary",
|
|
||||||
"crc32fast",
|
|
||||||
"indexmap 2.12.0",
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.8.0"
|
version = "5.8.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "friendolls-desktop"
|
name = "friendolls-desktop"
|
||||||
version = "0.1.4"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -50,11 +50,9 @@ enigo = { version = "0.6.1", features = ["wayland"] }
|
|||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
mlua = { version = "0.11", default-features = false, features = ["lua54", "vendored", "serde", "async"] }
|
mlua = { version = "0.11", default-features = false, features = ["lua54", "vendored", "serde", "async"] }
|
||||||
petpet = "2.4.3"
|
petpet = "2.4.3"
|
||||||
tauri-macros = "2.5.5"
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
tauri-plugin-positioner = "2"
|
tauri-plugin-positioner = "2"
|
||||||
tauri-plugin-updater = "2"
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.6.3"
|
objc2 = "0.6.3"
|
||||||
objc2-app-kit = { version = "0.3.2", features = [
|
objc2-app-kit = { version = "0.3.2", features = [
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"identifier": "desktop-capability",
|
|
||||||
"platforms": [
|
|
||||||
"macOS",
|
|
||||||
"windows",
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"windows": [
|
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"updater:default"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 974 B |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#fff</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,51 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Friendolls Sign-in Cancelled</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top, #fff0cf, transparent 45%),
|
|
||||||
linear-gradient(180deg, #fffdf7 0%, #f8f2e6 100%);
|
|
||||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
||||||
color: #55411d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: min(420px, calc(100vw - 32px));
|
|
||||||
padding: 32px 28px;
|
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
box-shadow: 0 24px 80px rgba(120, 95, 35, 0.14);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #7a6237;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Sign-in cancelled</h1>
|
|
||||||
<p>You can close this tab and return to Friendolls to try again.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Friendolls Sign-in Failed</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top, #ffd8d8, transparent 45%),
|
|
||||||
linear-gradient(180deg, #fff8f8 0%, #fceeee 100%);
|
|
||||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
||||||
color: #4d2323;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: min(420px, calc(100vw - 32px));
|
|
||||||
padding: 32px 28px;
|
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
box-shadow: 0 24px 80px rgba(135, 57, 57, 0.14);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #7c4a4a;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Sign-in failed</h1>
|
|
||||||
<p>Friendolls could not complete the browser handshake. Return to the app and try again.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Friendolls Sign-in Complete</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top, #d8f8ff, transparent 45%),
|
|
||||||
linear-gradient(180deg, #f8feff 0%, #eef8fb 100%);
|
|
||||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
||||||
color: #24414b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: min(420px, calc(100vw - 32px));
|
|
||||||
padding: 32px 28px;
|
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
box-shadow: 0 24px 80px rgba(55, 113, 130, 0.18);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #4b6973;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Signed in</h1>
|
|
||||||
<p>You can close this browser tab and return to Friendolls.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
use crate::init::lifecycle::validate_server_health;
|
use crate::init::lifecycle::{construct_user_session, validate_server_health};
|
||||||
use crate::services::auth::get_session_token;
|
use crate::services::auth::get_session_token;
|
||||||
use crate::services::session::construct_user_session;
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
lock_r,
|
lock_r,
|
||||||
models::{app_data::UserData, app_state::{AppState, NekoPosition}},
|
models::app_data::UserData,
|
||||||
services::{
|
services::{presence_modules::models::ModuleMetadata, presence_state::PresenceStateSnapshot},
|
||||||
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||||
app_state,
|
|
||||||
friends,
|
|
||||||
neko_positions,
|
|
||||||
presence_modules::models::ModuleMetadata,
|
|
||||||
sprite,
|
|
||||||
},
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -36,44 +29,6 @@ pub fn get_modules() -> Result<Vec<ModuleMetadata>, String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn get_active_doll_sprite_base64() -> Result<Option<String>, String> {
|
pub fn get_presence_state() -> Result<PresenceStateSnapshot, String> {
|
||||||
sprite::get_active_doll_sprite_base64()
|
Ok(crate::services::presence_state::get_presence_state_snapshot())
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn get_friend_active_doll_sprites_base64() -> Result<friends::FriendActiveDollSpritesDto, String>
|
|
||||||
{
|
|
||||||
friends::sync_active_doll_sprites_from_app_data();
|
|
||||||
Ok(friends::get_active_doll_sprites_snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn get_app_state() -> Result<AppState, String> {
|
|
||||||
Ok(app_state::get_snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn get_neko_positions() -> Result<neko_positions::NekoPositionsDto, String> {
|
|
||||||
Ok(neko_positions::get_snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
|
||||||
app_state::set_scene_setup_nekos_position(nekos_position);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
|
||||||
app_state::set_scene_setup_nekos_opacity(nekos_opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
|
|
||||||
app_state::set_scene_setup_nekos_scale(nekos_scale);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,45 @@ pub async fn logout_and_restart() -> Result<(), String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn start_google_auth() -> Result<(), String> {
|
pub async fn login(email: String, password: String) -> Result<(), String> {
|
||||||
auth::start_browser_login("google")
|
auth::login_and_init_session(&email, &password)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn start_discord_auth() -> Result<(), String> {
|
pub async fn register(
|
||||||
auth::start_browser_login("discord")
|
email: String,
|
||||||
|
password: String,
|
||||||
|
name: Option<String>,
|
||||||
|
username: Option<String>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
auth::register(
|
||||||
|
&email,
|
||||||
|
&password,
|
||||||
|
name.as_deref(),
|
||||||
|
username.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn change_password(
|
||||||
|
current_password: String,
|
||||||
|
new_password: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
auth::change_password(¤t_password, &new_password)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn reset_password(old_password: String, new_password: String) -> Result<(), String> {
|
||||||
|
auth::reset_password(&old_password, &new_password)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
lock_w,
|
lock_w,
|
||||||
services::client_config::{load_app_config, open_config_window, save_app_config, AppConfig},
|
services::client_config_manager::{
|
||||||
|
load_app_config, open_config_manager_window, save_app_config, AppConfig,
|
||||||
|
},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +29,6 @@ pub fn save_client_config(config: AppConfig) -> Result<(), String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn open_client_config() -> Result<(), String> {
|
pub async fn open_client_config_manager() -> Result<(), String> {
|
||||||
open_config_window().map_err(|e| e.to_string())
|
open_config_manager_window().map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{is_active_doll, refresh_app_data, refresh_app_data_conditionally},
|
get_app_handle,
|
||||||
models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
|
models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
|
||||||
remotes::{dolls::DollsRemote, user::UserRemote},
|
remotes::{
|
||||||
services::app_data::AppDataRefreshScope,
|
dolls::DollsRemote,
|
||||||
|
user::UserRemote,
|
||||||
|
},
|
||||||
|
state::AppDataRefreshScope,
|
||||||
|
commands::{refresh_app_data, refresh_app_data_conditionally, is_active_doll},
|
||||||
};
|
};
|
||||||
|
use crate::commands::scene::get_user_active_doll;
|
||||||
|
use crate::services::app_events::UserActiveDollUpdated;
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
@@ -50,8 +57,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, Stri
|
|||||||
refresh_app_data_conditionally(
|
refresh_app_data_conditionally(
|
||||||
&[AppDataRefreshScope::Dolls],
|
&[AppDataRefreshScope::Dolls],
|
||||||
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
||||||
)
|
).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@@ -70,8 +76,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> {
|
|||||||
refresh_app_data_conditionally(
|
refresh_app_data_conditionally(
|
||||||
&[AppDataRefreshScope::Dolls],
|
&[AppDataRefreshScope::Dolls],
|
||||||
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
||||||
)
|
).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -86,6 +91,9 @@ pub async fn set_active_doll(doll_id: String) -> Result<(), String> {
|
|||||||
|
|
||||||
refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await;
|
refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await;
|
||||||
|
|
||||||
|
let active_doll = get_user_active_doll().ok().flatten();
|
||||||
|
let _ = UserActiveDollUpdated(active_doll).emit(get_app_handle());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,5 +107,8 @@ pub async fn remove_active_doll() -> Result<(), String> {
|
|||||||
|
|
||||||
refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await;
|
refresh_app_data(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]).await;
|
||||||
|
|
||||||
|
let active_doll = get_user_active_doll().ok().flatten();
|
||||||
|
let _ = UserActiveDollUpdated(active_doll).emit(get_app_handle());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::commands::refresh_app_data;
|
use crate::remotes::friends::FriendRemote;
|
||||||
use crate::models::friends::{
|
use crate::models::friends::{
|
||||||
FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, UserBasicDto,
|
FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, UserBasicDto,
|
||||||
};
|
};
|
||||||
use crate::remotes::friends::FriendRemote;
|
use crate::state::AppDataRefreshScope;
|
||||||
use crate::services::app_data::AppDataRefreshScope;
|
use crate::commands::refresh_app_data;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ pub mod config;
|
|||||||
pub mod dolls;
|
pub mod dolls;
|
||||||
pub mod friends;
|
pub mod friends;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod petpet;
|
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
|
pub mod petpet;
|
||||||
|
pub mod scene;
|
||||||
|
|
||||||
use crate::lock_r;
|
use crate::lock_r;
|
||||||
use crate::{
|
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||||
services::app_data::{init_app_data_scoped, AppDataRefreshScope},
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
|
|
||||||
/// Helper to execute a mutation operation and refresh app data scopes in the background.
|
/// Helper to execute a mutation operation and refresh app data scopes in the background.
|
||||||
|
|||||||
32
src-tauri/src/commands/scene.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::{
|
||||||
|
lock_r,
|
||||||
|
models::{dolls::DollDto, scene::SceneFriendNeko},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_user_active_doll() -> Result<Option<DollDto>, String> {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
|
||||||
|
let Some(user) = &guard.user_data.user else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(active_doll_id) = &user.active_doll_id else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(guard.user_data.dolls.as_ref().and_then(|dolls| {
|
||||||
|
dolls
|
||||||
|
.iter()
|
||||||
|
.find(|doll| doll.id == *active_doll_id)
|
||||||
|
.cloned()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_scene_friends() -> Result<Vec<SceneFriendNeko>, String> {
|
||||||
|
Ok(crate::services::scene_friends::get_scene_friends_snapshot())
|
||||||
|
}
|
||||||
@@ -3,7 +3,60 @@ use std::time::Duration;
|
|||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{models::health::HealthError, remotes::health::HealthRemote};
|
use crate::{
|
||||||
|
models::health::HealthError,
|
||||||
|
remotes::health::HealthRemote,
|
||||||
|
services::{
|
||||||
|
close_all_windows,
|
||||||
|
health_manager::open_health_manager_window,
|
||||||
|
health_monitor::{start_health_monitor, stop_health_monitor},
|
||||||
|
scene::open_scene_window,
|
||||||
|
ws::client::{clear_ws_client, establish_websocket_connection},
|
||||||
|
},
|
||||||
|
state::{
|
||||||
|
auth::{start_background_token_refresh, stop_background_token_refresh},
|
||||||
|
clear_app_data, init_app_data_scoped, AppDataRefreshScope,
|
||||||
|
},
|
||||||
|
system_tray::update_system_tray,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Connects the user profile and opens the scene window.
|
||||||
|
pub async fn construct_user_session() {
|
||||||
|
connect_user_profile().await;
|
||||||
|
close_all_windows();
|
||||||
|
open_scene_window();
|
||||||
|
update_system_tray(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnects the user profile and closes the scene window.
|
||||||
|
pub async fn destruct_user_session() {
|
||||||
|
disconnect_user_profile().await;
|
||||||
|
close_all_windows();
|
||||||
|
update_system_tray(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the user profile and establishes a WebSocket connection.
|
||||||
|
async fn connect_user_profile() {
|
||||||
|
init_app_data_scoped(AppDataRefreshScope::All).await;
|
||||||
|
establish_websocket_connection().await;
|
||||||
|
start_background_token_refresh().await;
|
||||||
|
start_health_monitor().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the user profile and WebSocket connection.
|
||||||
|
async fn disconnect_user_profile() {
|
||||||
|
stop_health_monitor();
|
||||||
|
stop_background_token_refresh();
|
||||||
|
clear_app_data();
|
||||||
|
clear_ws_client().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destructs the user session and show health manager window
|
||||||
|
/// with error message, offering troubleshooting options.
|
||||||
|
pub async fn handle_disasterous_failure(error_message: Option<String>) {
|
||||||
|
destruct_user_session().await;
|
||||||
|
open_health_manager_window(error_message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Pings the server's health endpoint a maximum of
|
/// Pings the server's health endpoint a maximum of
|
||||||
/// three times with a backoff of 500ms between
|
/// three times with a backoff of 500ms between
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
init::{lifecycle::validate_server_health, tracing::init_logging},
|
init::{
|
||||||
|
lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health},
|
||||||
|
tracing::init_logging,
|
||||||
|
},
|
||||||
services::{
|
services::{
|
||||||
app_update::update_app,
|
|
||||||
auth::get_session_token,
|
auth::get_session_token,
|
||||||
cursor::init_cursor_tracking,
|
cursor::init_cursor_tracking,
|
||||||
presence_modules::init_modules,
|
presence_modules::init_modules,
|
||||||
scene::{close_splash_window, open_splash_window},
|
scene::{close_splash_window, open_splash_window},
|
||||||
session::{construct_user_session, handle_disastrous_failure},
|
|
||||||
welcome::open_welcome_window,
|
welcome::open_welcome_window,
|
||||||
},
|
},
|
||||||
state::init_app_state,
|
state::init_app_state,
|
||||||
@@ -21,14 +22,13 @@ pub mod tracing;
|
|||||||
pub async fn launch_app() {
|
pub async fn launch_app() {
|
||||||
init_logging();
|
init_logging();
|
||||||
open_splash_window();
|
open_splash_window();
|
||||||
update_app().await;
|
|
||||||
init_app_state();
|
init_app_state();
|
||||||
init_system_tray();
|
init_system_tray();
|
||||||
init_cursor_tracking().await;
|
init_cursor_tracking().await;
|
||||||
init_modules();
|
init_modules();
|
||||||
|
|
||||||
if let Err(err) = validate_server_health().await {
|
if let Err(err) = validate_server_health().await {
|
||||||
handle_disastrous_failure(Some(err.to_string())).await;
|
handle_disasterous_failure(Some(err.to_string())).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use crate::services::{
|
use crate::{
|
||||||
|
commands::app_state::{get_modules, get_presence_state},
|
||||||
|
services::{
|
||||||
doll_editor::open_doll_editor_window,
|
doll_editor::open_doll_editor_window,
|
||||||
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use commands::scene::{get_scene_friends, get_user_active_doll};
|
||||||
use commands::app::{quit_app, restart_app, retry_connection};
|
use commands::app::{quit_app, restart_app, retry_connection};
|
||||||
use commands::app_state::{
|
use commands::app_state::{get_app_data, refresh_app_data};
|
||||||
get_active_doll_sprite_base64, get_app_data, get_app_state, get_neko_positions,
|
use commands::auth::{change_password, login, logout_and_restart, register, reset_password};
|
||||||
get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
|
use commands::config::{get_client_config, open_client_config_manager, save_client_config};
|
||||||
set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale,
|
|
||||||
};
|
|
||||||
use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth};
|
|
||||||
use commands::config::{get_client_config, open_client_config, save_client_config};
|
|
||||||
use commands::dolls::{
|
use commands::dolls::{
|
||||||
create_doll, delete_doll, get_doll, get_dolls, remove_active_doll, set_active_doll, update_doll,
|
create_doll, delete_doll, get_doll, get_dolls, remove_active_doll, set_active_doll, update_doll,
|
||||||
};
|
};
|
||||||
@@ -18,18 +18,19 @@ use commands::friends::{
|
|||||||
search_users, send_friend_request, sent_friend_requests, unfriend,
|
search_users, send_friend_request, sent_friend_requests, unfriend,
|
||||||
};
|
};
|
||||||
use commands::interaction::send_interaction_cmd;
|
use commands::interaction::send_interaction_cmd;
|
||||||
use commands::petpet::encode_pet_doll_gif_base64;
|
|
||||||
use commands::sprite::recolor_gif_base64;
|
use commands::sprite::recolor_gif_base64;
|
||||||
|
use commands::petpet::encode_pet_doll_gif_base64;
|
||||||
use specta_typescript::Typescript;
|
use specta_typescript::Typescript;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
|
use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, collect_commands, collect_events};
|
||||||
|
|
||||||
use crate::services::app_events::{
|
use crate::services::app_events::{
|
||||||
ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
|
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
||||||
EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendDisconnected,
|
FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted,
|
||||||
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, PresenceStateUpdated,
|
||||||
InteractionDeliveryFailed, InteractionReceived, NekoPositionsUpdated,
|
SceneFriendsUpdated,
|
||||||
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
||||||
|
SetInteractionOverlay, Unfriended, UserActiveDollUpdated, UserStatusChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
@@ -66,10 +67,6 @@ pub fn run() {
|
|||||||
.error_handling(ErrorHandlingMode::Throw)
|
.error_handling(ErrorHandlingMode::Throw)
|
||||||
.commands(collect_commands![
|
.commands(collect_commands![
|
||||||
get_app_data,
|
get_app_data,
|
||||||
get_app_state,
|
|
||||||
get_neko_positions,
|
|
||||||
get_active_doll_sprite_base64,
|
|
||||||
get_friend_active_doll_sprites_base64,
|
|
||||||
refresh_app_data,
|
refresh_app_data,
|
||||||
list_friends,
|
list_friends,
|
||||||
search_users,
|
search_users,
|
||||||
@@ -93,41 +90,43 @@ pub fn run() {
|
|||||||
retry_connection,
|
retry_connection,
|
||||||
get_client_config,
|
get_client_config,
|
||||||
save_client_config,
|
save_client_config,
|
||||||
open_client_config,
|
open_client_config_manager,
|
||||||
open_doll_editor_window,
|
open_doll_editor_window,
|
||||||
get_scene_interactive,
|
get_scene_interactive,
|
||||||
set_scene_interactive,
|
set_scene_interactive,
|
||||||
set_pet_menu_state,
|
set_pet_menu_state,
|
||||||
start_google_auth,
|
get_user_active_doll,
|
||||||
start_discord_auth,
|
get_scene_friends,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
change_password,
|
||||||
|
reset_password,
|
||||||
logout_and_restart,
|
logout_and_restart,
|
||||||
send_interaction_cmd,
|
send_interaction_cmd,
|
||||||
get_modules,
|
get_modules,
|
||||||
set_scene_setup_nekos_position,
|
get_presence_state
|
||||||
set_scene_setup_nekos_opacity,
|
|
||||||
set_scene_setup_nekos_scale
|
|
||||||
])
|
])
|
||||||
.events(collect_events![
|
.events(collect_events![
|
||||||
|
CursorMoved,
|
||||||
SceneInteractiveChanged,
|
SceneInteractiveChanged,
|
||||||
AppDataRefreshed,
|
AppDataRefreshed,
|
||||||
AppStateChanged,
|
|
||||||
NekoPositionsUpdated,
|
|
||||||
ActiveDollSpriteChanged,
|
|
||||||
SetInteractionOverlay,
|
SetInteractionOverlay,
|
||||||
EditDoll,
|
EditDoll,
|
||||||
CreateDoll,
|
CreateDoll,
|
||||||
UserStatusChanged,
|
UserStatusChanged,
|
||||||
|
UserActiveDollUpdated,
|
||||||
|
FriendCursorPositionUpdated,
|
||||||
FriendDisconnected,
|
FriendDisconnected,
|
||||||
FriendActiveDollChanged,
|
FriendActiveDollChanged,
|
||||||
FriendActiveDollSpritesUpdated,
|
|
||||||
FriendUserStatusChanged,
|
FriendUserStatusChanged,
|
||||||
|
PresenceStateUpdated,
|
||||||
|
SceneFriendsUpdated,
|
||||||
InteractionReceived,
|
InteractionReceived,
|
||||||
InteractionDeliveryFailed,
|
InteractionDeliveryFailed,
|
||||||
FriendRequestReceived,
|
FriendRequestReceived,
|
||||||
FriendRequestAccepted,
|
FriendRequestAccepted,
|
||||||
FriendRequestDenied,
|
FriendRequestDenied,
|
||||||
Unfriended,
|
Unfriended
|
||||||
AuthFlowUpdated
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -136,7 +135,6 @@ pub fn run() {
|
|||||||
.expect("Failed to export TypeScript bindings");
|
.expect("Failed to export TypeScript bindings");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_positioner::init())
|
.plugin(tauri_plugin_positioner::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum NekoPosition {
|
|
||||||
TopLeft,
|
|
||||||
Top,
|
|
||||||
TopRight,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
BottomLeft,
|
|
||||||
Bottom,
|
|
||||||
BottomRight,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SceneSetup {
|
|
||||||
pub nekos_position: Option<NekoPosition>,
|
|
||||||
pub nekos_opacity: f32,
|
|
||||||
pub nekos_scale: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SceneSetup {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
nekos_position: None,
|
|
||||||
nekos_opacity: 1.0,
|
|
||||||
nekos_scale: 1.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type, Default)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct AppState {
|
|
||||||
pub scene_setup: SceneSetup,
|
|
||||||
}
|
|
||||||
@@ -19,20 +19,6 @@ pub struct UserStatusPayload {
|
|||||||
pub state: UserStatusState,
|
pub state: UserStatusState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserStatusPayload {
|
|
||||||
pub fn has_presence_content(&self) -> bool {
|
|
||||||
self.presence_status
|
|
||||||
.title
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|title| !title.trim().is_empty())
|
|
||||||
|| self
|
|
||||||
.presence_status
|
|
||||||
.subtitle
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|subtitle| !subtitle.trim().is_empty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, Type)]
|
#[derive(Clone, Serialize, Deserialize, Debug, Type)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FriendUserStatusPayload {
|
pub struct FriendUserStatusPayload {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
pub mod app_data;
|
pub mod app_data;
|
||||||
pub mod app_state;
|
|
||||||
pub mod dolls;
|
pub mod dolls;
|
||||||
pub mod event_payloads;
|
pub mod event_payloads;
|
||||||
pub mod friends;
|
pub mod friends;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod remote_error;
|
pub mod remote_error;
|
||||||
|
pub mod scene;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
12
src-tauri/src/models/scene.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
use crate::{models::dolls::DollDto, services::cursor::CursorPositions};
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug, Type)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SceneFriendNeko {
|
||||||
|
pub id: String,
|
||||||
|
pub position: CursorPositions,
|
||||||
|
pub active_doll: DollDto,
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{lock_r, services::auth::with_auth, state::FDOLL, models::dolls::*};
|
||||||
use crate::models::remote_error::RemoteError;
|
use crate::models::remote_error::RemoteError;
|
||||||
use crate::{lock_r, models::dolls::*, services::auth::with_auth, state::FDOLL};
|
|
||||||
|
|
||||||
pub struct DollsRemote {
|
pub struct DollsRemote {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -19,8 +19,7 @@ impl DollsRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.network
|
.network.clients
|
||||||
.clients
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{lock_r, services::auth::with_auth, state::FDOLL, models::friends::*};
|
||||||
use crate::models::remote_error::RemoteError;
|
use crate::models::remote_error::RemoteError;
|
||||||
use crate::{lock_r, models::friends::*, services::auth::with_auth, state::FDOLL};
|
|
||||||
|
|
||||||
pub struct FriendRemote {
|
pub struct FriendRemote {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -19,8 +19,7 @@ impl FriendRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.network
|
.network.clients
|
||||||
.clients
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
use crate::{lock_r, models::health::*, state::FDOLL};
|
use crate::{lock_r, state::FDOLL, models::health::*};
|
||||||
|
|
||||||
pub struct HealthRemote {
|
pub struct HealthRemote {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -18,8 +18,7 @@ impl HealthRemote {
|
|||||||
.ok_or(HealthError::ConfigMissing("api_base_url"))?;
|
.ok_or(HealthError::ConfigMissing("api_base_url"))?;
|
||||||
|
|
||||||
let client = guard
|
let client = guard
|
||||||
.network
|
.network.clients
|
||||||
.clients
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.http_client.clone())
|
.map(|c| c.http_client.clone())
|
||||||
.ok_or(HealthError::ConfigMissing("http_client"))?;
|
.ok_or(HealthError::ConfigMissing("http_client"))?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use reqwest::{Client, Error};
|
use reqwest::{Client, Error};
|
||||||
|
|
||||||
use crate::{lock_r, models::user::*, services::auth::with_auth, state::FDOLL};
|
use crate::{lock_r, services::auth::with_auth, state::FDOLL, models::user::*};
|
||||||
|
|
||||||
pub struct UserRemote {
|
pub struct UserRemote {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -18,8 +18,7 @@ impl UserRemote {
|
|||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.clone(),
|
.clone(),
|
||||||
client: guard
|
client: guard
|
||||||
.network
|
.network.clients
|
||||||
.clients
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("App configuration error")
|
.expect("App configuration error")
|
||||||
.http_client
|
.http_client
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
use device_query::Keycode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum AcceleratorAction {
|
|
||||||
SceneInteractivity,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Type)]
|
|
||||||
pub struct KeyboardAccelerator {
|
|
||||||
#[serde(default)]
|
|
||||||
pub modifiers: Vec<AcceleratorModifier>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub key: Option<AcceleratorKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyboardAccelerator {
|
|
||||||
pub fn normalized(mut self) -> Self {
|
|
||||||
self.modifiers.sort_unstable();
|
|
||||||
self.modifiers.dedup();
|
|
||||||
|
|
||||||
if self.modifiers.is_empty() {
|
|
||||||
return Self::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum AcceleratorModifier {
|
|
||||||
Cmd,
|
|
||||||
Alt,
|
|
||||||
Ctrl,
|
|
||||||
Shift,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for KeyboardAccelerator {
|
|
||||||
fn default() -> Self {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
modifiers: vec![AcceleratorModifier::Cmd],
|
|
||||||
key: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
modifiers: vec![AcceleratorModifier::Alt],
|
|
||||||
key: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_accelerator_for_action(action: AcceleratorAction) -> KeyboardAccelerator {
|
|
||||||
match action {
|
|
||||||
AcceleratorAction::SceneInteractivity => KeyboardAccelerator::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_accelerators() -> std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator>
|
|
||||||
{
|
|
||||||
let mut map = std::collections::BTreeMap::new();
|
|
||||||
map.insert(
|
|
||||||
AcceleratorAction::SceneInteractivity,
|
|
||||||
default_accelerator_for_action(AcceleratorAction::SceneInteractivity),
|
|
||||||
);
|
|
||||||
map
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalize_accelerators(
|
|
||||||
mut accelerators: std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
|
||||||
) -> std::collections::BTreeMap<AcceleratorAction, KeyboardAccelerator> {
|
|
||||||
for value in accelerators.values_mut() {
|
|
||||||
*value = value.clone().normalized();
|
|
||||||
}
|
|
||||||
|
|
||||||
for action in [AcceleratorAction::SceneInteractivity] {
|
|
||||||
accelerators
|
|
||||||
.entry(action)
|
|
||||||
.or_insert_with(|| default_accelerator_for_action(action));
|
|
||||||
}
|
|
||||||
|
|
||||||
accelerators
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Type)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum AcceleratorKey {
|
|
||||||
A,
|
|
||||||
B,
|
|
||||||
C,
|
|
||||||
D,
|
|
||||||
E,
|
|
||||||
F,
|
|
||||||
G,
|
|
||||||
H,
|
|
||||||
I,
|
|
||||||
J,
|
|
||||||
K,
|
|
||||||
L,
|
|
||||||
M,
|
|
||||||
N,
|
|
||||||
O,
|
|
||||||
P,
|
|
||||||
Q,
|
|
||||||
R,
|
|
||||||
S,
|
|
||||||
T,
|
|
||||||
U,
|
|
||||||
V,
|
|
||||||
W,
|
|
||||||
X,
|
|
||||||
Y,
|
|
||||||
Z,
|
|
||||||
Num0,
|
|
||||||
Num1,
|
|
||||||
Num2,
|
|
||||||
Num3,
|
|
||||||
Num4,
|
|
||||||
Num5,
|
|
||||||
Num6,
|
|
||||||
Num7,
|
|
||||||
Num8,
|
|
||||||
Num9,
|
|
||||||
F1,
|
|
||||||
F2,
|
|
||||||
F3,
|
|
||||||
F4,
|
|
||||||
F5,
|
|
||||||
F6,
|
|
||||||
F7,
|
|
||||||
F8,
|
|
||||||
F9,
|
|
||||||
F10,
|
|
||||||
F11,
|
|
||||||
F12,
|
|
||||||
Enter,
|
|
||||||
Space,
|
|
||||||
Escape,
|
|
||||||
Tab,
|
|
||||||
Backspace,
|
|
||||||
Delete,
|
|
||||||
Insert,
|
|
||||||
Home,
|
|
||||||
End,
|
|
||||||
PageUp,
|
|
||||||
PageDown,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Minus,
|
|
||||||
Equal,
|
|
||||||
LeftBracket,
|
|
||||||
RightBracket,
|
|
||||||
BackSlash,
|
|
||||||
Semicolon,
|
|
||||||
Apostrophe,
|
|
||||||
Comma,
|
|
||||||
Dot,
|
|
||||||
Slash,
|
|
||||||
Grave,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contains_any(keys: &[Keycode], candidates: &[Keycode]) -> bool {
|
|
||||||
candidates.iter().any(|candidate| keys.contains(candidate))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_modifier(keys: &[Keycode], modifier: AcceleratorModifier) -> bool {
|
|
||||||
match modifier {
|
|
||||||
AcceleratorModifier::Cmd => contains_any(
|
|
||||||
keys,
|
|
||||||
&[
|
|
||||||
Keycode::Command,
|
|
||||||
Keycode::RCommand,
|
|
||||||
Keycode::LMeta,
|
|
||||||
Keycode::RMeta,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
AcceleratorModifier::Alt => contains_any(
|
|
||||||
keys,
|
|
||||||
&[
|
|
||||||
Keycode::LAlt,
|
|
||||||
Keycode::RAlt,
|
|
||||||
Keycode::LOption,
|
|
||||||
Keycode::ROption,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
AcceleratorModifier::Ctrl => contains_any(keys, &[Keycode::LControl, Keycode::RControl]),
|
|
||||||
AcceleratorModifier::Shift => contains_any(keys, &[Keycode::LShift, Keycode::RShift]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keycodes_for(key: AcceleratorKey) -> &'static [Keycode] {
|
|
||||||
match key {
|
|
||||||
AcceleratorKey::A => &[Keycode::A],
|
|
||||||
AcceleratorKey::B => &[Keycode::B],
|
|
||||||
AcceleratorKey::C => &[Keycode::C],
|
|
||||||
AcceleratorKey::D => &[Keycode::D],
|
|
||||||
AcceleratorKey::E => &[Keycode::E],
|
|
||||||
AcceleratorKey::F => &[Keycode::F],
|
|
||||||
AcceleratorKey::G => &[Keycode::G],
|
|
||||||
AcceleratorKey::H => &[Keycode::H],
|
|
||||||
AcceleratorKey::I => &[Keycode::I],
|
|
||||||
AcceleratorKey::J => &[Keycode::J],
|
|
||||||
AcceleratorKey::K => &[Keycode::K],
|
|
||||||
AcceleratorKey::L => &[Keycode::L],
|
|
||||||
AcceleratorKey::M => &[Keycode::M],
|
|
||||||
AcceleratorKey::N => &[Keycode::N],
|
|
||||||
AcceleratorKey::O => &[Keycode::O],
|
|
||||||
AcceleratorKey::P => &[Keycode::P],
|
|
||||||
AcceleratorKey::Q => &[Keycode::Q],
|
|
||||||
AcceleratorKey::R => &[Keycode::R],
|
|
||||||
AcceleratorKey::S => &[Keycode::S],
|
|
||||||
AcceleratorKey::T => &[Keycode::T],
|
|
||||||
AcceleratorKey::U => &[Keycode::U],
|
|
||||||
AcceleratorKey::V => &[Keycode::V],
|
|
||||||
AcceleratorKey::W => &[Keycode::W],
|
|
||||||
AcceleratorKey::X => &[Keycode::X],
|
|
||||||
AcceleratorKey::Y => &[Keycode::Y],
|
|
||||||
AcceleratorKey::Z => &[Keycode::Z],
|
|
||||||
AcceleratorKey::Num0 => &[Keycode::Key0],
|
|
||||||
AcceleratorKey::Num1 => &[Keycode::Key1],
|
|
||||||
AcceleratorKey::Num2 => &[Keycode::Key2],
|
|
||||||
AcceleratorKey::Num3 => &[Keycode::Key3],
|
|
||||||
AcceleratorKey::Num4 => &[Keycode::Key4],
|
|
||||||
AcceleratorKey::Num5 => &[Keycode::Key5],
|
|
||||||
AcceleratorKey::Num6 => &[Keycode::Key6],
|
|
||||||
AcceleratorKey::Num7 => &[Keycode::Key7],
|
|
||||||
AcceleratorKey::Num8 => &[Keycode::Key8],
|
|
||||||
AcceleratorKey::Num9 => &[Keycode::Key9],
|
|
||||||
AcceleratorKey::F1 => &[Keycode::F1],
|
|
||||||
AcceleratorKey::F2 => &[Keycode::F2],
|
|
||||||
AcceleratorKey::F3 => &[Keycode::F3],
|
|
||||||
AcceleratorKey::F4 => &[Keycode::F4],
|
|
||||||
AcceleratorKey::F5 => &[Keycode::F5],
|
|
||||||
AcceleratorKey::F6 => &[Keycode::F6],
|
|
||||||
AcceleratorKey::F7 => &[Keycode::F7],
|
|
||||||
AcceleratorKey::F8 => &[Keycode::F8],
|
|
||||||
AcceleratorKey::F9 => &[Keycode::F9],
|
|
||||||
AcceleratorKey::F10 => &[Keycode::F10],
|
|
||||||
AcceleratorKey::F11 => &[Keycode::F11],
|
|
||||||
AcceleratorKey::F12 => &[Keycode::F12],
|
|
||||||
AcceleratorKey::Enter => &[Keycode::Enter, Keycode::NumpadEnter],
|
|
||||||
AcceleratorKey::Space => &[Keycode::Space],
|
|
||||||
AcceleratorKey::Escape => &[Keycode::Escape],
|
|
||||||
AcceleratorKey::Tab => &[Keycode::Tab],
|
|
||||||
AcceleratorKey::Backspace => &[Keycode::Backspace],
|
|
||||||
AcceleratorKey::Delete => &[Keycode::Delete],
|
|
||||||
AcceleratorKey::Insert => &[Keycode::Insert],
|
|
||||||
AcceleratorKey::Home => &[Keycode::Home],
|
|
||||||
AcceleratorKey::End => &[Keycode::End],
|
|
||||||
AcceleratorKey::PageUp => &[Keycode::PageUp],
|
|
||||||
AcceleratorKey::PageDown => &[Keycode::PageDown],
|
|
||||||
AcceleratorKey::ArrowUp => &[Keycode::Up],
|
|
||||||
AcceleratorKey::ArrowDown => &[Keycode::Down],
|
|
||||||
AcceleratorKey::ArrowLeft => &[Keycode::Left],
|
|
||||||
AcceleratorKey::ArrowRight => &[Keycode::Right],
|
|
||||||
AcceleratorKey::Minus => &[Keycode::Minus],
|
|
||||||
AcceleratorKey::Equal => &[Keycode::Equal],
|
|
||||||
AcceleratorKey::LeftBracket => &[Keycode::LeftBracket],
|
|
||||||
AcceleratorKey::RightBracket => &[Keycode::RightBracket],
|
|
||||||
AcceleratorKey::BackSlash => &[Keycode::BackSlash],
|
|
||||||
AcceleratorKey::Semicolon => &[Keycode::Semicolon],
|
|
||||||
AcceleratorKey::Apostrophe => &[Keycode::Apostrophe],
|
|
||||||
AcceleratorKey::Comma => &[Keycode::Comma],
|
|
||||||
AcceleratorKey::Dot => &[Keycode::Dot],
|
|
||||||
AcceleratorKey::Slash => &[Keycode::Slash],
|
|
||||||
AcceleratorKey::Grave => &[Keycode::Grave],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_key(keys: &[Keycode], key: AcceleratorKey) -> bool {
|
|
||||||
contains_any(keys, keycodes_for(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pressed_modifiers(keys: &[Keycode]) -> Vec<AcceleratorModifier> {
|
|
||||||
let mut modifiers = Vec::new();
|
|
||||||
|
|
||||||
for modifier in [
|
|
||||||
AcceleratorModifier::Cmd,
|
|
||||||
AcceleratorModifier::Alt,
|
|
||||||
AcceleratorModifier::Ctrl,
|
|
||||||
AcceleratorModifier::Shift,
|
|
||||||
] {
|
|
||||||
if has_modifier(keys, modifier) {
|
|
||||||
modifiers.push(modifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modifiers
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_accelerator_active(keys: &[Keycode], accelerator: &KeyboardAccelerator) -> bool {
|
|
||||||
if pressed_modifiers(keys) != accelerator.modifiers {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
accelerator.key.map_or(true, |key| has_key(keys, key))
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
use crate::{get_app_handle, lock_w, state::FDOLL};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
pub fn update_display_dimensions_for_scene_state() {
|
|
||||||
let app_handle = get_app_handle();
|
|
||||||
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
|
|
||||||
let primary_monitor = {
|
|
||||||
let mut retry_count = 0;
|
|
||||||
let max_retries = 3;
|
|
||||||
loop {
|
|
||||||
match app_handle.primary_monitor() {
|
|
||||||
Ok(Some(monitor)) => {
|
|
||||||
info!("Primary monitor acquired for state initialization");
|
|
||||||
break Some(monitor);
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
retry_count += 1;
|
|
||||||
if retry_count >= max_retries {
|
|
||||||
warn!(
|
|
||||||
"No primary monitor found after {} retries during state init",
|
|
||||||
max_retries
|
|
||||||
);
|
|
||||||
break None;
|
|
||||||
}
|
|
||||||
warn!(
|
|
||||||
"Primary monitor not available during state init, retrying... ({}/{})",
|
|
||||||
retry_count, max_retries
|
|
||||||
);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
retry_count += 1;
|
|
||||||
if retry_count >= max_retries {
|
|
||||||
warn!("Failed to get primary monitor during state init: {}", error);
|
|
||||||
break None;
|
|
||||||
}
|
|
||||||
warn!(
|
|
||||||
"Error getting primary monitor during state init, retrying... ({}/{}): {}",
|
|
||||||
retry_count, max_retries, error
|
|
||||||
);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(monitor) = primary_monitor {
|
|
||||||
let monitor_dimensions = monitor.size();
|
|
||||||
let monitor_scale_factor = monitor.scale_factor();
|
|
||||||
let logical_monitor_dimensions: tauri::LogicalSize<i32> =
|
|
||||||
monitor_dimensions.to_logical(monitor_scale_factor);
|
|
||||||
|
|
||||||
guard.user_data.scene.display.screen_width = logical_monitor_dimensions.width;
|
|
||||||
guard.user_data.scene.display.screen_height = logical_monitor_dimensions.height;
|
|
||||||
guard.user_data.scene.display.monitor_scale_factor = monitor_scale_factor;
|
|
||||||
guard.user_data.scene.grid_size = 600;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Initialized global AppData with screen dimensions: {}x{}, scale: {}, grid: {}",
|
|
||||||
logical_monitor_dimensions.width,
|
|
||||||
logical_monitor_dimensions.height,
|
|
||||||
monitor_scale_factor,
|
|
||||||
guard.user_data.scene.grid_size
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
warn!("Could not initialize screen dimensions in global state - no monitor found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod display;
|
|
||||||
mod refresh;
|
|
||||||
|
|
||||||
pub use display::update_display_dimensions_for_scene_state;
|
|
||||||
pub use refresh::{clear_app_data, init_app_data_scoped, AppDataRefreshScope};
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
use std::{collections::HashSet, sync::LazyLock};
|
|
||||||
|
|
||||||
use tauri_plugin_dialog::MessageDialogBuilder;
|
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
|
||||||
use tauri_specta::Event as _;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
get_app_handle, lock_r, lock_w,
|
|
||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
|
||||||
services::{
|
|
||||||
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
|
|
||||||
friends, neko_positions, sprite,
|
|
||||||
},
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum AppDataRefreshScope {
|
|
||||||
All,
|
|
||||||
User,
|
|
||||||
Friends,
|
|
||||||
Dolls,
|
|
||||||
}
|
|
||||||
|
|
||||||
static REFRESH_IN_FLIGHT: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
|
||||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
|
||||||
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
|
||||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
|
||||||
|
|
||||||
pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
|
|
||||||
if in_flight.contains(&scope) {
|
|
||||||
let mut pending = REFRESH_PENDING.lock().await;
|
|
||||||
pending.insert(scope);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
in_flight.insert(scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Result<(), ()> = async {
|
|
||||||
let user_remote = UserRemote::new();
|
|
||||||
let friend_remote = FriendRemote::new();
|
|
||||||
let dolls_remote = DollsRemote::new();
|
|
||||||
|
|
||||||
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) {
|
|
||||||
match user_remote.get_user(None).await {
|
|
||||||
Ok(user) => {
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.user_data.user = Some(user);
|
|
||||||
drop(guard);
|
|
||||||
neko_positions::sync_from_app_data();
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("Failed to fetch user profile: {}", error);
|
|
||||||
show_refresh_error_dialog(
|
|
||||||
"Network Error",
|
|
||||||
"Failed to fetch user profile. You may be offline.",
|
|
||||||
);
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
scope,
|
|
||||||
AppDataRefreshScope::All | AppDataRefreshScope::Friends
|
|
||||||
) {
|
|
||||||
match friend_remote.get_friends().await {
|
|
||||||
Ok(friends_list) => {
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.user_data.friends = Some(friends_list);
|
|
||||||
drop(guard);
|
|
||||||
friends::sync_from_app_data();
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("Failed to fetch friends list: {}", error);
|
|
||||||
show_refresh_error_dialog(
|
|
||||||
"Network Error",
|
|
||||||
"Failed to fetch friends list. You may be offline.",
|
|
||||||
);
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) {
|
|
||||||
match dolls_remote.get_dolls().await {
|
|
||||||
Ok(dolls) => {
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.user_data.dolls = Some(dolls);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("Failed to fetch dolls list: {}", error);
|
|
||||||
show_refresh_error_dialog(
|
|
||||||
"Network Error",
|
|
||||||
"Failed to fetch dolls list. You may be offline.",
|
|
||||||
);
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_refresh_events(scope);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut in_flight = REFRESH_IN_FLIGHT.lock().await;
|
|
||||||
in_flight.remove(&scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rerun = {
|
|
||||||
let mut pending = REFRESH_PENDING.lock().await;
|
|
||||||
pending.remove(&scope)
|
|
||||||
};
|
|
||||||
|
|
||||||
if rerun {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_app_data() {
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.user_data.dolls = None;
|
|
||||||
guard.user_data.user = None;
|
|
||||||
guard.user_data.friends = None;
|
|
||||||
drop(guard);
|
|
||||||
friends::clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_refresh_events(scope: AppDataRefreshScope) {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
let app_data = guard.user_data.clone();
|
|
||||||
drop(guard);
|
|
||||||
|
|
||||||
if let Err(error) = AppDataRefreshed(app_data).emit(get_app_handle()) {
|
|
||||||
warn!("Failed to emit app-data-refreshed event: {}", error);
|
|
||||||
show_refresh_error_dialog(
|
|
||||||
"Sync Error",
|
|
||||||
"Could not broadcast refreshed data to the UI. Some data may be stale.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
scope,
|
|
||||||
AppDataRefreshScope::All | AppDataRefreshScope::User | AppDataRefreshScope::Dolls
|
|
||||||
) {
|
|
||||||
match sprite::get_active_doll_sprite_base64() {
|
|
||||||
Ok(sprite_b64) => {
|
|
||||||
if let Err(error) = ActiveDollSpriteChanged(sprite_b64).emit(get_app_handle()) {
|
|
||||||
warn!("Failed to emit active-doll-sprite-changed event: {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("Failed to generate active doll sprite: {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_refresh_error_dialog(title: &str, message: &str) {
|
|
||||||
let handle = get_app_handle();
|
|
||||||
MessageDialogBuilder::new(handle.dialog().clone(), title, message)
|
|
||||||
.kind(MessageDialogKind::Error)
|
|
||||||
.show(|_| {});
|
|
||||||
}
|
|
||||||
@@ -5,33 +5,24 @@ use tauri_specta::Event;
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
app_data::UserData,
|
app_data::UserData,
|
||||||
app_state::AppState,
|
dolls::DollDto,
|
||||||
event_payloads::{
|
event_payloads::{
|
||||||
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
||||||
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
||||||
FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
|
FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
|
||||||
},
|
},
|
||||||
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||||
|
scene::SceneFriendNeko,
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
cursor::CursorPositions, presence_state::PresenceStateSnapshot,
|
||||||
|
ws::OutgoingFriendCursorPayload,
|
||||||
},
|
},
|
||||||
services::{friends::FriendActiveDollSpritesDto, neko_positions::NekoPositionsDto},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[tauri_specta(event_name = "cursor-position")]
|
||||||
pub enum AuthFlowStatus {
|
pub struct CursorMoved(pub CursorPositions);
|
||||||
Started,
|
|
||||||
Succeeded,
|
|
||||||
Failed,
|
|
||||||
Cancelled,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct AuthFlowUpdatedPayload {
|
|
||||||
pub provider: String,
|
|
||||||
pub status: AuthFlowStatus,
|
|
||||||
pub message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "scene-interactive")]
|
#[tauri_specta(event_name = "scene-interactive")]
|
||||||
@@ -41,14 +32,6 @@ pub struct SceneInteractiveChanged(pub bool);
|
|||||||
#[tauri_specta(event_name = "app-data-refreshed")]
|
#[tauri_specta(event_name = "app-data-refreshed")]
|
||||||
pub struct AppDataRefreshed(pub UserData);
|
pub struct AppDataRefreshed(pub UserData);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "app-state-changed")]
|
|
||||||
pub struct AppStateChanged(pub AppState);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "active-doll-sprite-changed")]
|
|
||||||
pub struct ActiveDollSpriteChanged(pub Option<String>);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "set-interaction-overlay")]
|
#[tauri_specta(event_name = "set-interaction-overlay")]
|
||||||
pub struct SetInteractionOverlay(pub bool);
|
pub struct SetInteractionOverlay(pub bool);
|
||||||
@@ -66,8 +49,12 @@ pub struct CreateDoll;
|
|||||||
pub struct UserStatusChanged(pub UserStatusPayload);
|
pub struct UserStatusChanged(pub UserStatusPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "neko-positions")]
|
#[tauri_specta(event_name = "user-active-doll-updated")]
|
||||||
pub struct NekoPositionsUpdated(pub NekoPositionsDto);
|
pub struct UserActiveDollUpdated(pub Option<DollDto>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "friend-cursor-position")]
|
||||||
|
pub struct FriendCursorPositionUpdated(pub OutgoingFriendCursorPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-disconnected")]
|
#[tauri_specta(event_name = "friend-disconnected")]
|
||||||
@@ -77,14 +64,18 @@ pub struct FriendDisconnected(pub FriendDisconnectedPayload);
|
|||||||
#[tauri_specta(event_name = "friend-active-doll-changed")]
|
#[tauri_specta(event_name = "friend-active-doll-changed")]
|
||||||
pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload);
|
pub struct FriendActiveDollChanged(pub FriendActiveDollChangedPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "friend-active-doll-sprites-updated")]
|
|
||||||
pub struct FriendActiveDollSpritesUpdated(pub FriendActiveDollSpritesDto);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-user-status")]
|
#[tauri_specta(event_name = "friend-user-status")]
|
||||||
pub struct FriendUserStatusChanged(pub FriendUserStatusPayload);
|
pub struct FriendUserStatusChanged(pub FriendUserStatusPayload);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "scene-friends-updated")]
|
||||||
|
pub struct SceneFriendsUpdated(pub Vec<SceneFriendNeko>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "presence-state-updated")]
|
||||||
|
pub struct PresenceStateUpdated(pub PresenceStateSnapshot);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "interaction-received")]
|
#[tauri_specta(event_name = "interaction-received")]
|
||||||
pub struct InteractionReceived(pub InteractionPayloadDto);
|
pub struct InteractionReceived(pub InteractionPayloadDto);
|
||||||
@@ -108,7 +99,3 @@ pub struct FriendRequestDenied(pub FriendRequestDeniedPayload);
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "unfriended")]
|
#[tauri_specta(event_name = "unfriended")]
|
||||||
pub struct Unfriended(pub UnfriendedPayload);
|
pub struct Unfriended(pub UnfriendedPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "auth-flow-updated")]
|
|
||||||
pub struct AuthFlowUpdated(pub AuthFlowUpdatedPayload);
|
|
||||||
|
|||||||
@@ -1,29 +1,41 @@
|
|||||||
use tracing::error;
|
use tauri::Manager;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::services::window_manager::{
|
use crate::get_app_handle;
|
||||||
ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub static APP_MENU_WINDOW_LABEL: &str = "app_menu";
|
pub static APP_MENU_WINDOW_LABEL: &str = "app_menu";
|
||||||
|
|
||||||
pub fn open_app_menu_window() {
|
pub fn open_app_menu_window() {
|
||||||
let mut config = WindowConfig::regular_ui(APP_MENU_WINDOW_LABEL, "/app-menu", "Friendolls");
|
let app_handle = get_app_handle();
|
||||||
config.width = 400.0;
|
let existing_webview_window = app_handle.get_window(APP_MENU_WINDOW_LABEL);
|
||||||
config.height = 550.0;
|
|
||||||
config.resizable = true;
|
|
||||||
|
|
||||||
match ensure_window(&config, true, false) {
|
if let Some(window) = existing_webview_window {
|
||||||
Ok(EnsureWindowResult::Created(_)) => {}
|
window.show().unwrap();
|
||||||
Ok(EnsureWindowResult::Existing(_)) => {}
|
return;
|
||||||
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
|
||||||
error!(
|
|
||||||
"Failed to build {} window due to missing parent '{}': impossible state",
|
|
||||||
APP_MENU_WINDOW_LABEL, parent_label
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(EnsureWindowError::ShowExisting(e))
|
|
||||||
| Err(EnsureWindowError::SetParent(e))
|
match tauri::WebviewWindowBuilder::new(
|
||||||
| Err(EnsureWindowError::Build(e)) => {
|
app_handle,
|
||||||
|
APP_MENU_WINDOW_LABEL,
|
||||||
|
tauri::WebviewUrl::App("/app-menu".into()),
|
||||||
|
)
|
||||||
|
.title("Friendolls")
|
||||||
|
.inner_size(400.0, 550.0)
|
||||||
|
.resizable(true)
|
||||||
|
.maximizable(false)
|
||||||
|
.decorations(true)
|
||||||
|
.transparent(false)
|
||||||
|
.shadow(true)
|
||||||
|
.visible(true)
|
||||||
|
.skip_taskbar(false)
|
||||||
|
.always_on_top(false)
|
||||||
|
.visible_on_all_workspaces(false)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
info!("{} window builder succeeded", APP_MENU_WINDOW_LABEL);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e);
|
error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
use std::sync::{Arc, LazyLock, RwLock};
|
|
||||||
|
|
||||||
use tauri_specta::Event as _;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
get_app_handle, lock_r, lock_w,
|
|
||||||
models::app_state::{AppState, NekoPosition},
|
|
||||||
services::{app_events::AppStateChanged, neko_positions},
|
|
||||||
};
|
|
||||||
|
|
||||||
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
|
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
|
||||||
|
|
||||||
pub fn get_snapshot() -> AppState {
|
|
||||||
let guard = lock_r!(APP_STATE);
|
|
||||||
guard.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
|
||||||
let mut guard = lock_w!(APP_STATE);
|
|
||||||
guard.scene_setup.nekos_position = nekos_position;
|
|
||||||
emit_snapshot(&guard);
|
|
||||||
drop(guard);
|
|
||||||
neko_positions::refresh_from_scene_setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
|
||||||
let mut guard = lock_w!(APP_STATE);
|
|
||||||
guard.scene_setup.nekos_opacity = nekos_opacity.clamp(0.1, 1.0);
|
|
||||||
emit_snapshot(&guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
|
|
||||||
let mut guard = lock_w!(APP_STATE);
|
|
||||||
guard.scene_setup.nekos_scale = nekos_scale.clamp(0.5, 2.0);
|
|
||||||
emit_snapshot(&guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_snapshot(app_state: &AppState) {
|
|
||||||
if let Err(error) = AppStateChanged(app_state.clone()).emit(get_app_handle()) {
|
|
||||||
warn!("Failed to emit app-state-changed event: {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use tauri_plugin_updater::UpdaterExt;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::get_app_handle;
|
|
||||||
|
|
||||||
pub async fn update_app() {
|
|
||||||
let app = get_app_handle();
|
|
||||||
if let Some(update) = match match app.updater() {
|
|
||||||
Ok(it) => it,
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to get updater: {err:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.check()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(it) => it,
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to check for update: {err:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} {
|
|
||||||
let mut downloaded = 0;
|
|
||||||
|
|
||||||
match update
|
|
||||||
.download_and_install(
|
|
||||||
|chunk_length, content_length| {
|
|
||||||
downloaded += chunk_length;
|
|
||||||
println!("downloaded {downloaded} from {content_length:?}");
|
|
||||||
},
|
|
||||||
|| {
|
|
||||||
info!("download finished");
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(it) => it,
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to install update: {err:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("update installed");
|
|
||||||
app.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
504
src-tauri/src/services/auth.rs
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
use crate::get_app_handle;
|
||||||
|
use crate::init::lifecycle::construct_user_session;
|
||||||
|
use crate::services::scene::close_splash_window;
|
||||||
|
use crate::services::welcome::close_welcome_window;
|
||||||
|
use crate::state::auth::get_auth_pass_with_refresh;
|
||||||
|
use crate::{lock_r, lock_w, state::FDOLL};
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
|
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||||
|
use keyring::Entry;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
const SERVICE_NAME: &str = "friendolls";
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Keyring error: {0}")]
|
||||||
|
KeyringError(#[from] keyring::Error),
|
||||||
|
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
NetworkError(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("JSON serialization error: {0}")]
|
||||||
|
SerializationError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Invalid app configuration")]
|
||||||
|
InvalidConfig,
|
||||||
|
|
||||||
|
#[error("Failed to refresh token")]
|
||||||
|
RefreshFailed,
|
||||||
|
|
||||||
|
#[error("Request failed: {0}")]
|
||||||
|
RequestFailed(String),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct AuthPass {
|
||||||
|
pub access_token: String,
|
||||||
|
pub expires_in: u64,
|
||||||
|
pub issued_at: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LoginResponse {
|
||||||
|
#[serde(rename = "accessToken")]
|
||||||
|
access_token: String,
|
||||||
|
#[serde(rename = "expiresIn")]
|
||||||
|
expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RegisterResponse {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct LoginRequest<'a> {
|
||||||
|
email: &'a str,
|
||||||
|
password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct RegisterRequest<'a> {
|
||||||
|
email: &'a str,
|
||||||
|
password: &'a str,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
name: Option<&'a str>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
username: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ChangePasswordRequest<'a> {
|
||||||
|
#[serde(rename = "currentPassword")]
|
||||||
|
current_password: &'a str,
|
||||||
|
#[serde(rename = "newPassword")]
|
||||||
|
new_password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ResetPasswordRequest<'a> {
|
||||||
|
#[serde(rename = "oldPassword")]
|
||||||
|
old_password: &'a str,
|
||||||
|
#[serde(rename = "newPassword")]
|
||||||
|
new_password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_auth_pass(access_token: String, expires_in: u64) -> Result<AuthPass, AuthError> {
|
||||||
|
let issued_at = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| AuthError::RefreshFailed)?
|
||||||
|
.as_secs();
|
||||||
|
Ok(AuthPass {
|
||||||
|
access_token,
|
||||||
|
expires_in,
|
||||||
|
issued_at: Some(issued_at),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_session_token() -> Option<AuthPass> {
|
||||||
|
get_auth_pass_with_refresh().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_access_token() -> Option<String> {
|
||||||
|
get_session_token().await.map(|pass| pass.access_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_auth_pass(auth_pass: &AuthPass) -> Result<(), AuthError> {
|
||||||
|
let json = serde_json::to_string(auth_pass)?;
|
||||||
|
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||||
|
encoder
|
||||||
|
.write_all(json.as_bytes())
|
||||||
|
.map_err(|e| AuthError::SerializationError(serde_json::Error::io(e)))?;
|
||||||
|
let compressed = encoder
|
||||||
|
.finish()
|
||||||
|
.map_err(|e| AuthError::SerializationError(serde_json::Error::io(e)))?;
|
||||||
|
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
const CHUNK_SIZE: usize = 1200;
|
||||||
|
let chunks: Vec<&str> = encoded
|
||||||
|
.as_bytes()
|
||||||
|
.chunks(CHUNK_SIZE)
|
||||||
|
.map(|chunk| std::str::from_utf8(chunk).unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
||||||
|
count_entry.set_password(&chunks.len().to_string())?;
|
||||||
|
|
||||||
|
for (i, chunk) in chunks.iter().enumerate() {
|
||||||
|
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
||||||
|
entry.set_password(chunk)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
||||||
|
entry.set_password(&encoded)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_auth_pass() -> Result<Option<AuthPass>, AuthError> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let encoded = {
|
||||||
|
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
||||||
|
let chunk_count = match count_entry.get_password() {
|
||||||
|
Ok(count_str) => match count_str.parse::<usize>() {
|
||||||
|
Ok(count) => count,
|
||||||
|
Err(_) => {
|
||||||
|
error!("Invalid chunk count in keyring");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(keyring::Error::NoEntry) => {
|
||||||
|
info!("No auth pass found in keyring");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load chunk count from keyring");
|
||||||
|
return Err(AuthError::KeyringError(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoded = String::new();
|
||||||
|
for i in 0..chunk_count {
|
||||||
|
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
||||||
|
match entry.get_password() {
|
||||||
|
Ok(chunk) => encoded.push_str(&chunk),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load chunk {} from keyring", i);
|
||||||
|
return Err(AuthError::KeyringError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoded
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let encoded = {
|
||||||
|
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
||||||
|
match entry.get_password() {
|
||||||
|
Ok(pass) => pass,
|
||||||
|
Err(keyring::Error::NoEntry) => {
|
||||||
|
info!("No auth pass found in keyring");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load auth pass from keyring");
|
||||||
|
return Err(AuthError::KeyringError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let compressed = match URL_SAFE_NO_PAD.decode(&encoded) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to base64 decode auth pass from keyring: {}", e);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decoder = GzDecoder::new(&compressed[..]);
|
||||||
|
let mut json = String::new();
|
||||||
|
if let Err(e) = decoder.read_to_string(&mut json) {
|
||||||
|
error!("Failed to decompress auth pass from keyring: {}", e);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_pass: AuthPass = match serde_json::from_str(&json) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_e) => {
|
||||||
|
error!("Failed to decode auth pass from keyring");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(auth_pass))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_auth_pass() -> Result<(), AuthError> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
||||||
|
let chunk_count = match count_entry.get_password() {
|
||||||
|
Ok(count_str) => count_str.parse::<usize>().unwrap_or(0),
|
||||||
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..chunk_count {
|
||||||
|
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
||||||
|
let _ = entry.delete_credential();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = count_entry.delete_credential();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
||||||
|
let _ = entry.delete_credential();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logout() -> Result<(), AuthError> {
|
||||||
|
info!("Logging out user");
|
||||||
|
lock_w!(FDOLL).auth.auth_pass = None;
|
||||||
|
clear_auth_pass()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout_and_restart() -> Result<(), AuthError> {
|
||||||
|
logout()?;
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
app_handle.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn with_auth(request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||||
|
if let Some(token) = get_access_token().await {
|
||||||
|
request.header("Authorization", format!("Bearer {}", token))
|
||||||
|
} else {
|
||||||
|
request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(email: &str, password: &str) -> Result<AuthPass, AuthError> {
|
||||||
|
let (app_config, http_client) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
let clients = guard.network.clients.as_ref();
|
||||||
|
if clients.is_none() {
|
||||||
|
error!("Clients not initialized yet!");
|
||||||
|
return Err(AuthError::InvalidConfig);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
guard.app_config.clone(),
|
||||||
|
clients.unwrap().http_client.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = app_config
|
||||||
|
.api_base_url
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AuthError::InvalidConfig)?;
|
||||||
|
let url = format!("{}/auth/login", base_url);
|
||||||
|
|
||||||
|
let response = http_client
|
||||||
|
.post(url)
|
||||||
|
.json(&LoginRequest { email, password })
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response.text().await.unwrap_or_default();
|
||||||
|
return Err(AuthError::RequestFailed(format!(
|
||||||
|
"Status: {}, Body: {}",
|
||||||
|
status, error_text
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let login_response: LoginResponse = response.json().await?;
|
||||||
|
let auth_pass = build_auth_pass(login_response.access_token, login_response.expires_in)?;
|
||||||
|
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
||||||
|
save_auth_pass(&auth_pass)?;
|
||||||
|
Ok(auth_pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
name: Option<&str>,
|
||||||
|
username: Option<&str>,
|
||||||
|
) -> Result<String, AuthError> {
|
||||||
|
let (app_config, http_client) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
let clients = guard.network.clients.as_ref();
|
||||||
|
if clients.is_none() {
|
||||||
|
error!("Clients not initialized yet!");
|
||||||
|
return Err(AuthError::InvalidConfig);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
guard.app_config.clone(),
|
||||||
|
clients.unwrap().http_client.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = app_config
|
||||||
|
.api_base_url
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AuthError::InvalidConfig)?;
|
||||||
|
let url = format!("{}/auth/register", base_url);
|
||||||
|
|
||||||
|
let response = http_client
|
||||||
|
.post(url)
|
||||||
|
.json(&RegisterRequest {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
username,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response.text().await.unwrap_or_default();
|
||||||
|
return Err(AuthError::RequestFailed(format!(
|
||||||
|
"Status: {}, Body: {}",
|
||||||
|
status, error_text
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let register_response: RegisterResponse = response.json().await?;
|
||||||
|
Ok(register_response.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn change_password(
|
||||||
|
current_password: &str,
|
||||||
|
new_password: &str,
|
||||||
|
) -> Result<(), AuthError> {
|
||||||
|
let (app_config, http_client) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
let clients = guard.network.clients.as_ref();
|
||||||
|
if clients.is_none() {
|
||||||
|
error!("Clients not initialized yet!");
|
||||||
|
return Err(AuthError::InvalidConfig);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
guard.app_config.clone(),
|
||||||
|
clients.unwrap().http_client.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = app_config
|
||||||
|
.api_base_url
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AuthError::InvalidConfig)?;
|
||||||
|
let url = format!("{}/auth/change-password", base_url);
|
||||||
|
|
||||||
|
let response = with_auth(
|
||||||
|
http_client.post(url).json(&ChangePasswordRequest {
|
||||||
|
current_password,
|
||||||
|
new_password,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response.text().await.unwrap_or_default();
|
||||||
|
return Err(AuthError::RequestFailed(format!(
|
||||||
|
"Status: {}, Body: {}",
|
||||||
|
status, error_text
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset_password(old_password: &str, new_password: &str) -> Result<(), AuthError> {
|
||||||
|
let (app_config, http_client) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
let clients = guard.network.clients.as_ref();
|
||||||
|
if clients.is_none() {
|
||||||
|
error!("Clients not initialized yet!");
|
||||||
|
return Err(AuthError::InvalidConfig);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
guard.app_config.clone(),
|
||||||
|
clients.unwrap().http_client.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = app_config
|
||||||
|
.api_base_url
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AuthError::InvalidConfig)?;
|
||||||
|
let url = format!("{}/auth/reset-password", base_url);
|
||||||
|
|
||||||
|
let response = with_auth(
|
||||||
|
http_client.post(url).json(&ResetPasswordRequest {
|
||||||
|
old_password,
|
||||||
|
new_password,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response.text().await.unwrap_or_default();
|
||||||
|
return Err(AuthError::RequestFailed(format!(
|
||||||
|
"Status: {}, Body: {}",
|
||||||
|
status, error_text
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_token(access_token: &str) -> Result<AuthPass, AuthError> {
|
||||||
|
let (app_config, http_client) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
(
|
||||||
|
guard.app_config.clone(),
|
||||||
|
guard
|
||||||
|
.network
|
||||||
|
.clients
|
||||||
|
.as_ref()
|
||||||
|
.expect("clients present")
|
||||||
|
.http_client
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = app_config
|
||||||
|
.api_base_url
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AuthError::InvalidConfig)?;
|
||||||
|
let url = format!("{}/auth/refresh", base_url);
|
||||||
|
|
||||||
|
let response = http_client
|
||||||
|
.post(url)
|
||||||
|
.header("Authorization", format!("Bearer {}", access_token))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response.text().await.unwrap_or_default();
|
||||||
|
error!("Token refresh failed with status {}: {}", status, error_text);
|
||||||
|
return Err(AuthError::RefreshFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let refresh_response: LoginResponse = response.json().await?;
|
||||||
|
let auth_pass = build_auth_pass(refresh_response.access_token, refresh_response.expires_in)?;
|
||||||
|
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
||||||
|
save_auth_pass(&auth_pass)?;
|
||||||
|
Ok(auth_pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_and_init_session(email: &str, password: &str) -> Result<(), AuthError> {
|
||||||
|
login(email, password).await?;
|
||||||
|
close_welcome_window();
|
||||||
|
tauri::async_runtime::spawn(async {
|
||||||
|
construct_user_session().await;
|
||||||
|
close_splash_window();
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{lock_r, lock_w, state::FDOLL};
|
|
||||||
|
|
||||||
use super::storage::{build_auth_pass, save_auth_pass, AuthError, AuthPass};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct StartSsoResponse {
|
|
||||||
pub state: String,
|
|
||||||
#[serde(rename = "authorizeUrl")]
|
|
||||||
pub authorize_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct TokenResponse {
|
|
||||||
#[serde(rename = "accessToken")]
|
|
||||||
access_token: String,
|
|
||||||
#[serde(rename = "expiresIn")]
|
|
||||||
expires_in: u64,
|
|
||||||
#[serde(rename = "refreshToken")]
|
|
||||||
refresh_token: String,
|
|
||||||
#[serde(rename = "refreshExpiresIn")]
|
|
||||||
refresh_expires_in: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct StartSsoRequest<'a> {
|
|
||||||
provider: &'a str,
|
|
||||||
#[serde(rename = "redirectUri")]
|
|
||||||
redirect_uri: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct ExchangeSsoCodeRequest<'a> {
|
|
||||||
code: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct RefreshTokenRequest<'a> {
|
|
||||||
#[serde(rename = "refreshToken")]
|
|
||||||
refresh_token: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct LogoutRequest<'a> {
|
|
||||||
#[serde(rename = "refreshToken")]
|
|
||||||
refresh_token: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_http_context() -> Result<(String, reqwest::Client), AuthError> {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
let clients = guard.network.clients.as_ref().ok_or_else(|| {
|
|
||||||
error!("Clients not initialized yet!");
|
|
||||||
AuthError::InvalidConfig
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let base_url = guard
|
|
||||||
.app_config
|
|
||||||
.api_base_url
|
|
||||||
.clone()
|
|
||||||
.ok_or(AuthError::InvalidConfig)?;
|
|
||||||
|
|
||||||
Ok((base_url, clients.http_client.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_success(response: reqwest::Response) -> Result<reqwest::Response, AuthError> {
|
|
||||||
if response.status().is_success() {
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let error_text = response.text().await.unwrap_or_default();
|
|
||||||
Err(AuthError::RequestFailed(format!(
|
|
||||||
"Status: {}, Body: {}",
|
|
||||||
status, error_text
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn with_auth(request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
|
||||||
if let Some(token) = super::session::get_access_token().await {
|
|
||||||
request.header("Authorization", format!("Bearer {}", token))
|
|
||||||
} else {
|
|
||||||
request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_sso(provider: &str, redirect_uri: &str) -> Result<StartSsoResponse, AuthError> {
|
|
||||||
let (base_url, http_client) = auth_http_context()?;
|
|
||||||
let response = http_client
|
|
||||||
.post(format!("{}/auth/sso/start", base_url))
|
|
||||||
.json(&StartSsoRequest {
|
|
||||||
provider,
|
|
||||||
redirect_uri,
|
|
||||||
})
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ensure_success(response)
|
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(AuthError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn exchange_sso_code(code: &str) -> Result<AuthPass, AuthError> {
|
|
||||||
let (base_url, http_client) = auth_http_context()?;
|
|
||||||
let response = http_client
|
|
||||||
.post(format!("{}/auth/sso/exchange", base_url))
|
|
||||||
.json(&ExchangeSsoCodeRequest { code })
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let token_response: TokenResponse = ensure_success(response).await?.json().await?;
|
|
||||||
build_auth_pass(
|
|
||||||
token_response.access_token,
|
|
||||||
token_response.expires_in,
|
|
||||||
token_response.refresh_token,
|
|
||||||
token_response.refresh_expires_in,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, AuthError> {
|
|
||||||
let (base_url, http_client) = auth_http_context()?;
|
|
||||||
let response = http_client
|
|
||||||
.post(format!("{}/auth/refresh", base_url))
|
|
||||||
.json(&RefreshTokenRequest { refresh_token })
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let token_response: TokenResponse = ensure_success(response).await?.json().await?;
|
|
||||||
let auth_pass = build_auth_pass(
|
|
||||||
token_response.access_token,
|
|
||||||
token_response.expires_in,
|
|
||||||
token_response.refresh_token,
|
|
||||||
token_response.refresh_expires_in,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
|
||||||
save_auth_pass(&auth_pass)?;
|
|
||||||
Ok(auth_pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout_remote(refresh_token: &str) -> Result<(), AuthError> {
|
|
||||||
let (base_url, http_client) = auth_http_context()?;
|
|
||||||
let response = http_client
|
|
||||||
.post(format!("{}/auth/logout", base_url))
|
|
||||||
.json(&LogoutRequest { refresh_token })
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ensure_success(response).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn persist_auth_pass(auth_pass: &AuthPass) -> Result<(), AuthError> {
|
|
||||||
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
|
||||||
save_auth_pass(auth_pass)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
|
||||||
use tauri_specta::Event as _;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
use tracing::{error, info, warn};
|
|
||||||
|
|
||||||
use crate::get_app_handle;
|
|
||||||
use crate::services::app_events::{AuthFlowStatus, AuthFlowUpdated, AuthFlowUpdatedPayload};
|
|
||||||
use crate::state::{begin_auth_flow, clear_auth_flow_state, is_auth_flow_active};
|
|
||||||
use crate::{lock_r, state::FDOLL};
|
|
||||||
|
|
||||||
use super::api::{exchange_sso_code, persist_auth_pass, start_sso};
|
|
||||||
use super::storage::AuthError;
|
|
||||||
|
|
||||||
static AUTH_SUCCESS_HTML: &str = include_str!("../../assets/auth-success.html");
|
|
||||||
static AUTH_CANCELLED_HTML: &str = include_str!("../../assets/auth-cancelled.html");
|
|
||||||
static AUTH_FAILED_HTML: &str = include_str!("../../assets/auth-failed.html");
|
|
||||||
|
|
||||||
pub struct OAuthCallbackParams {
|
|
||||||
pub state: String,
|
|
||||||
pub result: OAuthCallbackResult,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum OAuthCallbackResult {
|
|
||||||
Code(String),
|
|
||||||
Error { message: String, cancelled: bool },
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PendingOAuthCallback {
|
|
||||||
stream: TcpStream,
|
|
||||||
params: OAuthCallbackParams,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_browser_auth_flow(provider: &str) -> Result<(), AuthError> {
|
|
||||||
let (flow_id, cancel_token) = begin_auth_flow();
|
|
||||||
|
|
||||||
let bind_addr = "127.0.0.1:0";
|
|
||||||
let std_listener = std::net::TcpListener::bind(bind_addr)
|
|
||||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
|
||||||
std_listener
|
|
||||||
.set_nonblocking(true)
|
|
||||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
|
||||||
let local_addr = std_listener
|
|
||||||
.local_addr()
|
|
||||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
|
||||||
|
|
||||||
let redirect_uri = format!("http://127.0.0.1:{}/callback", local_addr.port());
|
|
||||||
let start_response = match start_sso(provider, &redirect_uri).await {
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(err) => {
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let listener = TcpListener::from_std(std_listener)
|
|
||||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
|
||||||
let expected_state = start_response.state.clone();
|
|
||||||
let auth_url = match start_response.authorize_url.clone() {
|
|
||||||
Some(authorize_url) => authorize_url,
|
|
||||||
None => match build_authorize_url(provider, &start_response.state) {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(err) => {
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let provider_name = provider.to_string();
|
|
||||||
|
|
||||||
if let Err(err) = get_app_handle().opener().open_url(auth_url, None::<&str>) {
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
emit_auth_flow_event(
|
|
||||||
provider,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some("Friendolls could not open your browser for sign-in.".to_string()),
|
|
||||||
);
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_auth_flow_event(provider, AuthFlowStatus::Started, None);
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
match listen_for_callback(listener, cancel_token.clone()).await {
|
|
||||||
Ok(mut callback) => {
|
|
||||||
if !is_auth_flow_active(flow_id) {
|
|
||||||
let _ = write_html_response(&mut callback.stream, AUTH_CANCELLED_HTML).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if callback.params.state != expected_state {
|
|
||||||
error!("SSO state mismatch");
|
|
||||||
if let Err(err) =
|
|
||||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth failure response: {}", err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some("Sign-in verification failed. Please try again.".to_string()),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match callback.params.result {
|
|
||||||
OAuthCallbackResult::Code(code) => {
|
|
||||||
let auth_pass = match exchange_sso_code(&code).await {
|
|
||||||
Ok(auth_pass) => auth_pass,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to exchange SSO code: {}", err);
|
|
||||||
if let Err(write_err) =
|
|
||||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth failure response: {}", write_err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some(
|
|
||||||
"Friendolls could not complete sign-in. Please try again."
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_auth_flow_active(flow_id) {
|
|
||||||
let _ = write_html_response(&mut callback.stream, AUTH_CANCELLED_HTML)
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = persist_auth_pass(&auth_pass) {
|
|
||||||
error!("Failed to persist SSO auth pass: {}", err);
|
|
||||||
if let Err(write_err) =
|
|
||||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth failure response: {}", write_err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some(
|
|
||||||
"Friendolls could not complete sign-in. Please try again."
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = super::session::finish_login_session().await {
|
|
||||||
error!("Failed to finalize desktop login session: {}", err);
|
|
||||||
if let Err(write_err) =
|
|
||||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth failure response: {}", write_err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some(
|
|
||||||
"Signed in, but Friendolls could not open your session."
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
} else {
|
|
||||||
if let Err(err) =
|
|
||||||
write_html_response(&mut callback.stream, AUTH_SUCCESS_HTML).await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth success response: {}", err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(&provider_name, AuthFlowStatus::Succeeded, None);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OAuthCallbackResult::Error { message, cancelled } => {
|
|
||||||
let response_html = if cancelled {
|
|
||||||
AUTH_CANCELLED_HTML
|
|
||||||
} else {
|
|
||||||
AUTH_FAILED_HTML
|
|
||||||
};
|
|
||||||
if let Err(err) =
|
|
||||||
write_html_response(&mut callback.stream, response_html).await
|
|
||||||
{
|
|
||||||
warn!("Failed to write auth callback response: {}", err);
|
|
||||||
}
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
if cancelled {
|
|
||||||
AuthFlowStatus::Cancelled
|
|
||||||
} else {
|
|
||||||
AuthFlowStatus::Failed
|
|
||||||
},
|
|
||||||
Some(message),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(AuthError::Cancelled) => {
|
|
||||||
info!("Auth flow cancelled");
|
|
||||||
if is_auth_flow_active(flow_id) {
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Cancelled,
|
|
||||||
Some("Sign-in was cancelled.".to_string()),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("Auth callback listener failed: {}", err);
|
|
||||||
if is_auth_flow_active(flow_id) {
|
|
||||||
emit_auth_flow_event(
|
|
||||||
&provider_name,
|
|
||||||
AuthFlowStatus::Failed,
|
|
||||||
Some(auth_flow_error_message(&err)),
|
|
||||||
);
|
|
||||||
clear_auth_flow_state(flow_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_authorize_url(provider: &str, state: &str) -> Result<String, AuthError> {
|
|
||||||
let base_url = lock_r!(FDOLL)
|
|
||||||
.app_config
|
|
||||||
.api_base_url
|
|
||||||
.clone()
|
|
||||||
.ok_or(AuthError::InvalidConfig)?;
|
|
||||||
|
|
||||||
let mut parsed = url::Url::parse(&base_url)
|
|
||||||
.map_err(|e| AuthError::RequestFailed(format!("Invalid API base URL: {}", e)))?;
|
|
||||||
let existing_path = parsed.path().trim_end_matches('/');
|
|
||||||
parsed.set_path(&format!("{}/auth/sso/{}", existing_path, provider));
|
|
||||||
let query = url::form_urlencoded::Serializer::new(String::new())
|
|
||||||
.append_pair("state", state)
|
|
||||||
.finish();
|
|
||||||
parsed.set_query(Some(&query));
|
|
||||||
|
|
||||||
Ok(parsed.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn listen_for_callback(
|
|
||||||
listener: TcpListener,
|
|
||||||
cancel_token: CancellationToken,
|
|
||||||
) -> Result<PendingOAuthCallback, AuthError> {
|
|
||||||
let timeout = tokio::time::Duration::from_secs(300);
|
|
||||||
let start = tokio::time::Instant::now();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
if elapsed >= timeout {
|
|
||||||
return Err(AuthError::CallbackTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = timeout - elapsed;
|
|
||||||
let accepted = tokio::select! {
|
|
||||||
_ = cancel_token.cancelled() => return Err(AuthError::Cancelled),
|
|
||||||
result = tokio::time::timeout(remaining, listener.accept()) => result,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (mut stream, _) = match accepted {
|
|
||||||
Ok(Ok(value)) => value,
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
warn!("Accept error in auth callback listener: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => return Err(AuthError::CallbackTimeout),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(params) = parse_callback(&mut stream).await? {
|
|
||||||
return Ok(PendingOAuthCallback { stream, params });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn parse_callback(stream: &mut TcpStream) -> Result<Option<OAuthCallbackParams>, AuthError> {
|
|
||||||
let mut buffer = [0; 4096];
|
|
||||||
let bytes_read = match stream.read(&mut buffer).await {
|
|
||||||
Ok(value) if value > 0 => value,
|
|
||||||
_ => return Ok(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
|
|
||||||
let first_line = request.lines().next().unwrap_or_default();
|
|
||||||
let mut parts = first_line.split_whitespace();
|
|
||||||
|
|
||||||
match (parts.next(), parts.next()) {
|
|
||||||
(Some("GET"), Some(path)) if path.starts_with("/callback") => {
|
|
||||||
let parsed = url::Url::parse(&format!("http://localhost{}", path))
|
|
||||||
.map_err(|e| AuthError::RequestFailed(e.to_string()))?;
|
|
||||||
let params: std::collections::HashMap<_, _> =
|
|
||||||
parsed.query_pairs().into_owned().collect();
|
|
||||||
|
|
||||||
let state = params
|
|
||||||
.get("state")
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| AuthError::MissingParameter("state".to_string()))?;
|
|
||||||
if let Some(error_code) = params.get("error") {
|
|
||||||
let message = oauth_error_message(error_code, params.get("error_description"));
|
|
||||||
return Ok(Some(OAuthCallbackParams {
|
|
||||||
state,
|
|
||||||
result: OAuthCallbackResult::Error {
|
|
||||||
message,
|
|
||||||
cancelled: is_oauth_cancellation(error_code),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let code = params
|
|
||||||
.get("code")
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| AuthError::MissingParameter("code".to_string()))?;
|
|
||||||
|
|
||||||
Ok(Some(OAuthCallbackParams {
|
|
||||||
state,
|
|
||||||
result: OAuthCallbackResult::Code(code),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
(Some("GET"), Some("/health")) => {
|
|
||||||
stream
|
|
||||||
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
|
|
||||||
.await?;
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
stream
|
|
||||||
.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
|
|
||||||
.await?;
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_auth_flow_event(provider: &str, status: AuthFlowStatus, message: Option<String>) {
|
|
||||||
if let Err(err) = AuthFlowUpdated(AuthFlowUpdatedPayload {
|
|
||||||
provider: provider.to_string(),
|
|
||||||
status,
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
.emit(get_app_handle())
|
|
||||||
{
|
|
||||||
warn!("Failed to emit auth flow event: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_flow_error_message(err: &AuthError) -> String {
|
|
||||||
match err {
|
|
||||||
AuthError::Cancelled => "Sign-in was cancelled.".to_string(),
|
|
||||||
AuthError::CallbackTimeout => "Sign-in timed out. Please try again.".to_string(),
|
|
||||||
AuthError::MissingParameter(_) => {
|
|
||||||
"Friendolls did not receive a complete sign-in response. Please try again.".to_string()
|
|
||||||
}
|
|
||||||
_ => "Friendolls could not complete sign-in. Please try again.".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn oauth_error_message(error_code: &str, description: Option<&String>) -> String {
|
|
||||||
if let Some(description) = description.filter(|description| !description.is_empty()) {
|
|
||||||
return description.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_oauth_cancellation(error_code) {
|
|
||||||
"Sign-in was cancelled.".to_string()
|
|
||||||
} else {
|
|
||||||
"The sign-in provider reported an error. Please try again.".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_oauth_cancellation(error_code: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
error_code,
|
|
||||||
"access_denied" | "user_cancelled" | "authorization_cancelled"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_html_response(stream: &mut TcpStream, html: &str) -> Result<(), AuthError> {
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n\r\n{}",
|
|
||||||
html.len(),
|
|
||||||
html
|
|
||||||
);
|
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
stream.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
mod api;
|
|
||||||
mod flow;
|
|
||||||
mod session;
|
|
||||||
mod storage;
|
|
||||||
|
|
||||||
pub use api::{refresh_token, with_auth};
|
|
||||||
pub use session::{get_access_token, get_session_token, logout_and_restart, start_browser_login};
|
|
||||||
pub use storage::{clear_auth_pass, load_auth_pass, AuthPass};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use tokio::time::{timeout, Duration};
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::get_app_handle;
|
|
||||||
use crate::services::{
|
|
||||||
scene::close_splash_window, session::construct_user_session, welcome::close_welcome_window,
|
|
||||||
};
|
|
||||||
use crate::state::auth::get_auth_pass_with_refresh;
|
|
||||||
use crate::{lock_w, state::FDOLL};
|
|
||||||
|
|
||||||
use super::storage::{clear_auth_pass, AuthError, AuthPass};
|
|
||||||
|
|
||||||
pub async fn get_session_token() -> Option<AuthPass> {
|
|
||||||
get_auth_pass_with_refresh().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_access_token() -> Option<String> {
|
|
||||||
get_session_token().await.map(|pass| pass.access_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout() -> Result<(), AuthError> {
|
|
||||||
info!("Logging out user");
|
|
||||||
let refresh_token = lock_w!(FDOLL)
|
|
||||||
.auth
|
|
||||||
.auth_pass
|
|
||||||
.take()
|
|
||||||
.and_then(|pass| pass.refresh_token);
|
|
||||||
clear_auth_pass()?;
|
|
||||||
|
|
||||||
if let Some(refresh_token) = refresh_token {
|
|
||||||
match timeout(
|
|
||||||
Duration::from_secs(5),
|
|
||||||
super::api::logout_remote(&refresh_token),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(())) => {}
|
|
||||||
Ok(Err(err)) => info!("Failed to revoke refresh token on server: {}", err),
|
|
||||||
Err(_) => info!("Timed out while revoking refresh token on server"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn logout_and_restart() -> Result<(), AuthError> {
|
|
||||||
logout().await?;
|
|
||||||
let app_handle = get_app_handle();
|
|
||||||
app_handle.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn finish_login_session() -> Result<(), AuthError> {
|
|
||||||
close_welcome_window();
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
construct_user_session().await;
|
|
||||||
close_splash_window();
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_browser_login(provider: &str) -> Result<(), AuthError> {
|
|
||||||
super::flow::start_browser_auth_flow(provider).await
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
|
||||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
|
||||||
use keyring::Entry;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
const SERVICE_NAME: &str = "friendolls";
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum AuthError {
|
|
||||||
#[error("Keyring error: {0}")]
|
|
||||||
KeyringError(#[from] keyring::Error),
|
|
||||||
|
|
||||||
#[error("Network error: {0}")]
|
|
||||||
NetworkError(#[from] reqwest::Error),
|
|
||||||
|
|
||||||
#[error("JSON serialization error: {0}")]
|
|
||||||
SerializationError(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error("Invalid app configuration")]
|
|
||||||
InvalidConfig,
|
|
||||||
|
|
||||||
#[error("Failed to refresh token")]
|
|
||||||
RefreshFailed,
|
|
||||||
|
|
||||||
#[error("Request failed: {0}")]
|
|
||||||
RequestFailed(String),
|
|
||||||
|
|
||||||
#[error("Missing callback parameter: {0}")]
|
|
||||||
MissingParameter(String),
|
|
||||||
|
|
||||||
#[error("Authentication flow cancelled")]
|
|
||||||
Cancelled,
|
|
||||||
|
|
||||||
#[error("Callback timeout - no response received")]
|
|
||||||
CallbackTimeout,
|
|
||||||
|
|
||||||
#[error("Server binding failed: {0}")]
|
|
||||||
ServerBindError(String),
|
|
||||||
|
|
||||||
#[error("Failed to open auth portal: {0}")]
|
|
||||||
OpenPortalFailed(#[from] tauri_plugin_opener::Error),
|
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
|
||||||
IoError(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct AuthPass {
|
|
||||||
pub access_token: String,
|
|
||||||
pub expires_in: u64,
|
|
||||||
#[serde(default)]
|
|
||||||
pub refresh_token: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub refresh_expires_in: Option<u64>,
|
|
||||||
pub issued_at: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_auth_pass(
|
|
||||||
access_token: String,
|
|
||||||
expires_in: u64,
|
|
||||||
refresh_token: String,
|
|
||||||
refresh_expires_in: u64,
|
|
||||||
) -> Result<AuthPass, AuthError> {
|
|
||||||
let issued_at = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map_err(|_| AuthError::RefreshFailed)?
|
|
||||||
.as_secs();
|
|
||||||
Ok(AuthPass {
|
|
||||||
access_token,
|
|
||||||
expires_in,
|
|
||||||
refresh_token: Some(refresh_token),
|
|
||||||
refresh_expires_in: Some(refresh_expires_in),
|
|
||||||
issued_at: Some(issued_at),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_auth_pass(auth_pass: &AuthPass) -> Result<(), AuthError> {
|
|
||||||
let json = serde_json::to_string(auth_pass)?;
|
|
||||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
|
||||||
encoder
|
|
||||||
.write_all(json.as_bytes())
|
|
||||||
.map_err(|e| AuthError::SerializationError(serde_json::Error::io(e)))?;
|
|
||||||
let compressed = encoder
|
|
||||||
.finish()
|
|
||||||
.map_err(|e| AuthError::SerializationError(serde_json::Error::io(e)))?;
|
|
||||||
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
const CHUNK_SIZE: usize = 1200;
|
|
||||||
let chunks: Vec<&str> = encoded
|
|
||||||
.as_bytes()
|
|
||||||
.chunks(CHUNK_SIZE)
|
|
||||||
.map(|chunk| std::str::from_utf8(chunk).expect("base64 chunk is valid utf-8"))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
|
||||||
count_entry.set_password(&chunks.len().to_string())?;
|
|
||||||
|
|
||||||
for (i, chunk) in chunks.iter().enumerate() {
|
|
||||||
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
|
||||||
entry.set_password(chunk)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
|
||||||
entry.set_password(&encoded)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_auth_pass() -> Result<Option<AuthPass>, AuthError> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let encoded = {
|
|
||||||
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
|
||||||
let chunk_count = match count_entry.get_password() {
|
|
||||||
Ok(count_str) => match count_str.parse::<usize>() {
|
|
||||||
Ok(count) => count,
|
|
||||||
Err(_) => {
|
|
||||||
error!("Invalid chunk count in keyring");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(keyring::Error::NoEntry) => {
|
|
||||||
info!("No auth pass found in keyring");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to load chunk count from keyring");
|
|
||||||
return Err(AuthError::KeyringError(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut encoded = String::new();
|
|
||||||
for i in 0..chunk_count {
|
|
||||||
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(chunk) => encoded.push_str(&chunk),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to load chunk {} from keyring", i);
|
|
||||||
return Err(AuthError::KeyringError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encoded
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
let encoded = {
|
|
||||||
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(pass) => pass,
|
|
||||||
Err(keyring::Error::NoEntry) => {
|
|
||||||
info!("No auth pass found in keyring");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to load auth pass from keyring");
|
|
||||||
return Err(AuthError::KeyringError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let compressed = match URL_SAFE_NO_PAD.decode(&encoded) {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to base64 decode auth pass from keyring: {}", e);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut decoder = GzDecoder::new(&compressed[..]);
|
|
||||||
let mut json = String::new();
|
|
||||||
if let Err(e) = decoder.read_to_string(&mut json) {
|
|
||||||
error!("Failed to decompress auth pass from keyring: {}", e);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let auth_pass: AuthPass = match serde_json::from_str(&json) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => {
|
|
||||||
error!("Failed to decode auth pass from keyring");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if auth_pass.refresh_token.is_none() || auth_pass.refresh_expires_in.is_none() {
|
|
||||||
info!("Loaded legacy auth pass without refresh token support");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(auth_pass))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_auth_pass() -> Result<(), AuthError> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let count_entry = Entry::new(SERVICE_NAME, "auth_pass_count")?;
|
|
||||||
let chunk_count = match count_entry.get_password() {
|
|
||||||
Ok(count_str) => count_str.parse::<usize>().unwrap_or(0),
|
|
||||||
Err(_) => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for i in 0..chunk_count {
|
|
||||||
let entry = Entry::new(SERVICE_NAME, &format!("auth_pass_{}", i))?;
|
|
||||||
let _ = entry.delete_credential();
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = count_entry.delete_credential();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let entry = Entry::new(SERVICE_NAME, "auth_pass")?;
|
|
||||||
let _ = entry.delete_credential();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
mod store;
|
|
||||||
mod window;
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub use store::{load_app_config, save_app_config};
|
|
||||||
pub use window::open_config_window;
|
|
||||||
|
|
||||||
pub use crate::services::accelerators::{
|
|
||||||
default_accelerator_for_action, default_accelerators, normalize_accelerators,
|
|
||||||
AcceleratorAction, KeyboardAccelerator,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug, Type)]
|
|
||||||
pub struct AppConfig {
|
|
||||||
pub api_base_url: Option<String>,
|
|
||||||
pub debug_mode: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub accelerators: BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
pub fn normalized(mut self) -> Self {
|
|
||||||
self.accelerators = normalize_accelerators(self.accelerators);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn accelerator_for(&self, action: AcceleratorAction) -> KeyboardAccelerator {
|
|
||||||
self.accelerators
|
|
||||||
.get(&action)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| default_accelerator_for_action(action))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
api_base_url: None,
|
|
||||||
debug_mode: false,
|
|
||||||
accelerators: default_accelerators(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for AppConfig {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct AppConfigSerde {
|
|
||||||
api_base_url: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
debug_mode: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
accelerators: BTreeMap<AcceleratorAction, KeyboardAccelerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = AppConfigSerde::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
api_base_url: value.api_base_url,
|
|
||||||
debug_mode: value.debug_mode,
|
|
||||||
accelerators: value.accelerators,
|
|
||||||
}
|
|
||||||
.normalized())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ClientConfigError {
|
|
||||||
#[error("failed to resolve app config dir: {0}")]
|
|
||||||
ResolvePath(tauri::Error),
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("failed to parse client config: {0}")]
|
|
||||||
Parse(#[from] serde_json::Error),
|
|
||||||
#[error("failed to run on main thread: {0}")]
|
|
||||||
Dispatch(#[from] tauri::Error),
|
|
||||||
#[error("failed to build client config window: {0}")]
|
|
||||||
Window(tauri::Error),
|
|
||||||
#[error("failed to show client config window: {0}")]
|
|
||||||
ShowWindow(tauri::Error),
|
|
||||||
#[error("missing required parent window: {0}")]
|
|
||||||
MissingParent(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static CLIENT_CONFIG_WINDOW_LABEL: &str = "client_config";
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::services::window_manager::{
|
|
||||||
ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{ClientConfigError, CLIENT_CONFIG_WINDOW_LABEL};
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn open_config_window() -> Result<(), ClientConfigError> {
|
|
||||||
let mut config = WindowConfig::regular_ui(
|
|
||||||
CLIENT_CONFIG_WINDOW_LABEL,
|
|
||||||
"/client-config",
|
|
||||||
"Advanced Configuration",
|
|
||||||
);
|
|
||||||
config.width = 300.0;
|
|
||||||
config.height = 420.0;
|
|
||||||
config.visible = false;
|
|
||||||
|
|
||||||
match ensure_window(&config, true, true) {
|
|
||||||
Ok(EnsureWindowResult::Created(window)) => {
|
|
||||||
if let Err(e) = window.show() {
|
|
||||||
error!("Failed to show client config window: {}", e);
|
|
||||||
return Err(ClientConfigError::ShowWindow(e));
|
|
||||||
}
|
|
||||||
if let Err(e) = window.set_focus() {
|
|
||||||
error!("Failed to focus client config window: {e}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(EnsureWindowResult::Existing(_)) => Ok(()),
|
|
||||||
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
|
||||||
error!(
|
|
||||||
"Missing parent '{}' for client config window: impossible state",
|
|
||||||
parent_label
|
|
||||||
);
|
|
||||||
Err(ClientConfigError::MissingParent(parent_label))
|
|
||||||
}
|
|
||||||
Err(EnsureWindowError::ShowExisting(e)) => {
|
|
||||||
error!("Failed to show client config window: {e}");
|
|
||||||
Err(ClientConfigError::ShowWindow(e))
|
|
||||||
}
|
|
||||||
Err(EnsureWindowError::SetParent(e)) => {
|
|
||||||
error!("Failed to set parent for client config window: {}", e);
|
|
||||||
Err(ClientConfigError::Window(e))
|
|
||||||
}
|
|
||||||
Err(EnsureWindowError::Build(e)) => {
|
|
||||||
error!("Failed to build client config window: {}", e);
|
|
||||||
Err(ClientConfigError::Window(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tracing::warn;
|
use thiserror::Error;
|
||||||
|
use tracing::{error, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
|
|
||||||
use super::{AppConfig, ClientConfigError};
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub api_base_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ClientConfigError {
|
||||||
|
#[error("failed to resolve app config dir: {0}")]
|
||||||
|
ResolvePath(tauri::Error),
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("failed to parse client config: {0}")]
|
||||||
|
Parse(#[from] serde_json::Error),
|
||||||
|
#[error("failed to run on main thread: {0}")]
|
||||||
|
Dispatch(#[from] tauri::Error),
|
||||||
|
#[error("failed to build client config manager window: {0}")]
|
||||||
|
Window(tauri::Error),
|
||||||
|
#[error("failed to show client config manager window: {0}")]
|
||||||
|
ShowWindow(tauri::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static CLIENT_CONFIG_MANAGER_WINDOW_LABEL: &str = "client_config_manager";
|
||||||
const CONFIG_FILENAME: &str = "client_config.json";
|
const CONFIG_FILENAME: &str = "client_config.json";
|
||||||
const DEFAULT_API_BASE_URL: &str = "https://api.friendolls.adamcv.com";
|
const DEFAULT_API_BASE_URL: &str = "https://api.fdolls.adamcv.com";
|
||||||
|
|
||||||
fn config_file_path(app_handle: &tauri::AppHandle) -> Result<PathBuf, ClientConfigError> {
|
fn config_file_path(app_handle: &tauri::AppHandle) -> Result<PathBuf, ClientConfigError> {
|
||||||
let dir = app_handle
|
let dir = app_handle
|
||||||
@@ -48,14 +71,12 @@ fn sanitize(mut config: AppConfig) -> AppConfig {
|
|||||||
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
||||||
.map(|v| strip_trailing_slash(&v));
|
.map(|v| strip_trailing_slash(&v));
|
||||||
|
|
||||||
config.normalized()
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_app_config() -> AppConfig {
|
pub fn default_app_config() -> AppConfig {
|
||||||
AppConfig {
|
AppConfig {
|
||||||
api_base_url: Some(DEFAULT_API_BASE_URL.to_string()),
|
api_base_url: Some(DEFAULT_API_BASE_URL.to_string()),
|
||||||
debug_mode: false,
|
|
||||||
..AppConfig::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,3 +126,48 @@ pub fn save_app_config(config: AppConfig) -> Result<AppConfig, ClientConfigError
|
|||||||
|
|
||||||
Ok(sanitized)
|
Ok(sanitized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_config_manager_window() -> Result<(), ClientConfigError> {
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_MANAGER_WINDOW_LABEL);
|
||||||
|
|
||||||
|
if let Some(window) = existing_webview_window {
|
||||||
|
if let Err(e) = window.show() {
|
||||||
|
error!("Failed to show client config manager window: {e}");
|
||||||
|
return Err(ClientConfigError::ShowWindow(e));
|
||||||
|
}
|
||||||
|
if let Err(e) = window.set_focus() {
|
||||||
|
error!("Failed to focus client config manager window: {e}");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match tauri::WebviewWindowBuilder::new(
|
||||||
|
app_handle,
|
||||||
|
CLIENT_CONFIG_MANAGER_WINDOW_LABEL,
|
||||||
|
tauri::WebviewUrl::App("/client-config-manager".into()),
|
||||||
|
)
|
||||||
|
.title("Advanced Configuration")
|
||||||
|
.inner_size(300.0, 420.0)
|
||||||
|
.resizable(false)
|
||||||
|
.maximizable(false)
|
||||||
|
.visible(false)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(window) => {
|
||||||
|
if let Err(e) = window.show() {
|
||||||
|
error!("Failed to show client config manager window: {}", e);
|
||||||
|
return Err(ClientConfigError::ShowWindow(e));
|
||||||
|
}
|
||||||
|
if let Err(e) = window.set_focus() {
|
||||||
|
error!("Failed to focus client config manager window: {e}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to build client config manager window: {}", e);
|
||||||
|
Err(ClientConfigError::Window(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ use tokio::sync::mpsc;
|
|||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
get_app_handle,
|
||||||
lock_r,
|
lock_r,
|
||||||
services::{neko_positions, ws::report_cursor_data},
|
services::app_events::CursorMoved,
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -62,7 +64,8 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize cursor tracking.
|
/// Initialize cursor tracking. Broadcasts cursor
|
||||||
|
/// position changes via `cursor-position` event.
|
||||||
pub async fn init_cursor_tracking() {
|
pub async fn init_cursor_tracking() {
|
||||||
info!("start_cursor_tracking called");
|
info!("start_cursor_tracking called");
|
||||||
|
|
||||||
@@ -90,19 +93,22 @@ async fn init_cursor_tracking_i() -> Result<(), String> {
|
|||||||
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||||
|
|
||||||
// Spawn the consumer task
|
// Spawn the consumer task
|
||||||
// This task handles WebSocket reporting and local position projection updates.
|
// This task handles WebSocket reporting and local broadcasting.
|
||||||
// It runs independently of the device event loop.
|
// It runs independently of the device event loop.
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
info!("Cursor event consumer started");
|
info!("Cursor event consumer started");
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
|
||||||
while let Some(positions) = rx.recv().await {
|
while let Some(positions) = rx.recv().await {
|
||||||
let mapped_for_ws = positions.mapped.clone();
|
let mapped_for_ws = positions.mapped.clone();
|
||||||
|
|
||||||
// 1. WebSocket reporting
|
// 1. WebSocket reporting
|
||||||
report_cursor_data(mapped_for_ws).await;
|
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
||||||
|
|
||||||
// 2. Update unified neko positions projection
|
// 2. Broadcast to local windows
|
||||||
neko_positions::update_self_cursor(positions);
|
if let Err(e) = CursorMoved(positions).emit(app_handle) {
|
||||||
|
error!("Failed to emit cursor position event: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
warn!("Cursor event consumer stopped (channel closed)");
|
warn!("Cursor event consumer stopped (channel closed)");
|
||||||
});
|
});
|
||||||
|
|||||||