Compare commits
2 Commits
main
...
2dcc202540
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dcc202540 | |||
| 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",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -16,7 +16,6 @@
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-updater": "~2.10.0",
|
||||
"tailwindcss": "^4.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
15
pnpm-lock.yaml
generated
@@ -17,9 +17,6 @@ importers:
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: ^2
|
||||
version: 2.5.2
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: ~2.10.0
|
||||
version: 2.10.0
|
||||
tailwindcss:
|
||||
specifier: ^4.1.17
|
||||
version: 4.1.17
|
||||
@@ -470,9 +467,6 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
||||
|
||||
@@ -550,9 +544,6 @@ packages:
|
||||
'@tauri-apps/plugin-opener@2.5.2':
|
||||
resolution: {integrity: sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.10.0':
|
||||
resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==}
|
||||
|
||||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
@@ -1146,8 +1137,6 @@ snapshots:
|
||||
tailwindcss: 4.1.17
|
||||
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/cli-darwin-arm64@2.9.4':
|
||||
@@ -1201,10 +1190,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@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/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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
@@ -810,17 +801,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -1154,17 +1134,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
@@ -1240,7 +1209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "friendolls-desktop"
|
||||
version = "0.1.4"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"device_query",
|
||||
@@ -1269,13 +1238,11 @@ dependencies = [
|
||||
"strum",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-macros",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-positioner",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-specta",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
@@ -1529,10 +1496,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1542,11 +1507,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1878,7 +1841,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1949,9 +1911,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ico"
|
||||
version = "0.5.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"png",
|
||||
@@ -2339,7 +2301,6 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2369,12 +2330,6 @@ version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "550.0.0"
|
||||
@@ -2480,12 +2435,6 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2890,18 +2839,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
@@ -3052,20 +2989,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3457,61 +3380,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.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]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
@@ -3750,8 +3618,6 @@ dependencies = [
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3759,7 +3625,6 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -3769,7 +3634,6 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3890,7 +3754,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
@@ -3903,7 +3766,6 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -4642,17 +4504,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4735,9 +4586,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.5.5"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29"
|
||||
checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -4762,9 +4613,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.5.5"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7"
|
||||
checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -4893,38 +4744,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.1"
|
||||
@@ -5007,9 +4826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.8.3"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d"
|
||||
checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -5168,21 +4987,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
@@ -5913,16 +5717,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.1"
|
||||
@@ -5967,15 +5761,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.0"
|
||||
@@ -6702,16 +6487,6 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "xkbcommon"
|
||||
version = "0.9.0"
|
||||
@@ -6908,18 +6683,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "zvariant"
|
||||
version = "5.8.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "friendolls-desktop"
|
||||
version = "0.1.4"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -50,11 +50,9 @@ enigo = { version = "0.6.1", features = ["wayland"] }
|
||||
lazy_static = "1.5.0"
|
||||
mlua = { version = "0.11", default-features = false, features = ["lua54", "vendored", "serde", "async"] }
|
||||
petpet = "2.4.3"
|
||||
tauri-macros = "2.5.5"
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-positioner = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6.3"
|
||||
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::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::session::construct_user_session;
|
||||
use tracing::info;
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use crate::{
|
||||
lock_r,
|
||||
models::{app_data::UserData, app_state::{AppState, NekoPosition}},
|
||||
services::{
|
||||
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
||||
app_state,
|
||||
friends,
|
||||
neko_positions,
|
||||
presence_modules::models::ModuleMetadata,
|
||||
sprite,
|
||||
},
|
||||
state::FDOLL,
|
||||
models::app_data::UserData,
|
||||
services::{presence_modules::models::ModuleMetadata, presence_state::PresenceStateSnapshot},
|
||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -36,44 +29,6 @@ pub fn get_modules() -> Result<Vec<ModuleMetadata>, String> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_active_doll_sprite_base64() -> Result<Option<String>, String> {
|
||||
sprite::get_active_doll_sprite_base64()
|
||||
}
|
||||
|
||||
#[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);
|
||||
pub fn get_presence_state() -> Result<PresenceStateSnapshot, String> {
|
||||
Ok(crate::services::presence_state::get_presence_state_snapshot())
|
||||
}
|
||||
|
||||
@@ -8,16 +8,45 @@ pub async fn logout_and_restart() -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn start_google_auth() -> Result<(), String> {
|
||||
auth::start_browser_login("google")
|
||||
pub async fn login(email: String, password: String) -> Result<(), String> {
|
||||
auth::login_and_init_session(&email, &password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn start_discord_auth() -> Result<(), String> {
|
||||
auth::start_browser_login("discord")
|
||||
pub async fn register(
|
||||
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
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -27,6 +29,6 @@ pub fn save_client_config(config: AppConfig) -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn open_client_config() -> Result<(), String> {
|
||||
open_config_window().map_err(|e| e.to_string())
|
||||
pub async fn open_client_config_manager() -> Result<(), String> {
|
||||
open_config_manager_window().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use crate::{
|
||||
commands::{is_active_doll, refresh_app_data, refresh_app_data_conditionally},
|
||||
get_app_handle,
|
||||
models::dolls::{CreateDollDto, DollDto, UpdateDollDto},
|
||||
remotes::{dolls::DollsRemote, user::UserRemote},
|
||||
services::app_data::AppDataRefreshScope,
|
||||
remotes::{
|
||||
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]
|
||||
#[specta::specta]
|
||||
@@ -50,8 +57,7 @@ pub async fn update_doll(id: String, dto: UpdateDollDto) -> Result<DollDto, Stri
|
||||
refresh_app_data_conditionally(
|
||||
&[AppDataRefreshScope::Dolls],
|
||||
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
||||
)
|
||||
.await;
|
||||
).await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -70,8 +76,7 @@ pub async fn delete_doll(id: String) -> Result<(), String> {
|
||||
refresh_app_data_conditionally(
|
||||
&[AppDataRefreshScope::Dolls],
|
||||
is_active.then_some(&[AppDataRefreshScope::User, AppDataRefreshScope::Friends]),
|
||||
)
|
||||
.await;
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -86,6 +91,9 @@ pub async fn set_active_doll(doll_id: String) -> Result<(), String> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -99,5 +107,8 @@ pub async fn remove_active_doll() -> Result<(), String> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::commands::refresh_app_data;
|
||||
use crate::remotes::friends::FriendRemote;
|
||||
use crate::models::friends::{
|
||||
FriendRequestResponseDto, FriendshipResponseDto, SendFriendRequestDto, UserBasicDto,
|
||||
};
|
||||
use crate::remotes::friends::FriendRemote;
|
||||
use crate::services::app_data::AppDataRefreshScope;
|
||||
use crate::state::AppDataRefreshScope;
|
||||
use crate::commands::refresh_app_data;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -5,14 +5,12 @@ pub mod config;
|
||||
pub mod dolls;
|
||||
pub mod friends;
|
||||
pub mod interaction;
|
||||
pub mod petpet;
|
||||
pub mod sprite;
|
||||
pub mod petpet;
|
||||
pub mod scene;
|
||||
|
||||
use crate::lock_r;
|
||||
use crate::{
|
||||
services::app_data::{init_app_data_scoped, AppDataRefreshScope},
|
||||
state::FDOLL,
|
||||
};
|
||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||
use tauri::async_runtime;
|
||||
|
||||
/// 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 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
|
||||
/// three times with a backoff of 500ms between
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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::{
|
||||
app_update::update_app,
|
||||
auth::get_session_token,
|
||||
cursor::init_cursor_tracking,
|
||||
presence_modules::init_modules,
|
||||
scene::{close_splash_window, open_splash_window},
|
||||
session::{construct_user_session, handle_disastrous_failure},
|
||||
welcome::open_welcome_window,
|
||||
},
|
||||
state::init_app_state,
|
||||
@@ -21,14 +22,13 @@ pub mod tracing;
|
||||
pub async fn launch_app() {
|
||||
init_logging();
|
||||
open_splash_window();
|
||||
update_app().await;
|
||||
init_app_state();
|
||||
init_system_tray();
|
||||
init_cursor_tracking().await;
|
||||
init_modules();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::services::{
|
||||
doll_editor::open_doll_editor_window,
|
||||
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
||||
use crate::{
|
||||
commands::app_state::{get_modules, get_presence_state},
|
||||
services::{
|
||||
doll_editor::open_doll_editor_window,
|
||||
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_state::{
|
||||
get_active_doll_sprite_base64, get_app_data, get_app_state, get_neko_positions,
|
||||
get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
|
||||
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::app_state::{get_app_data, refresh_app_data};
|
||||
use commands::auth::{change_password, login, logout_and_restart, register, reset_password};
|
||||
use commands::config::{get_client_config, open_client_config_manager, save_client_config};
|
||||
use commands::dolls::{
|
||||
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,
|
||||
};
|
||||
use commands::interaction::send_interaction_cmd;
|
||||
use commands::petpet::encode_pet_doll_gif_base64;
|
||||
use commands::sprite::recolor_gif_base64;
|
||||
use commands::petpet::encode_pet_doll_gif_base64;
|
||||
use specta_typescript::Typescript;
|
||||
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::{
|
||||
ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
|
||||
EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendDisconnected,
|
||||
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||
InteractionDeliveryFailed, InteractionReceived, NekoPositionsUpdated,
|
||||
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
||||
AppDataRefreshed, CreateDoll, CursorMoved, EditDoll, FriendActiveDollChanged,
|
||||
FriendCursorPositionUpdated, FriendDisconnected, FriendRequestAccepted,
|
||||
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, PresenceStateUpdated,
|
||||
SceneFriendsUpdated,
|
||||
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
||||
SetInteractionOverlay, Unfriended, UserActiveDollUpdated, UserStatusChanged,
|
||||
};
|
||||
|
||||
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)
|
||||
.commands(collect_commands![
|
||||
get_app_data,
|
||||
get_app_state,
|
||||
get_neko_positions,
|
||||
get_active_doll_sprite_base64,
|
||||
get_friend_active_doll_sprites_base64,
|
||||
refresh_app_data,
|
||||
list_friends,
|
||||
search_users,
|
||||
@@ -93,41 +90,43 @@ pub fn run() {
|
||||
retry_connection,
|
||||
get_client_config,
|
||||
save_client_config,
|
||||
open_client_config,
|
||||
open_client_config_manager,
|
||||
open_doll_editor_window,
|
||||
get_scene_interactive,
|
||||
set_scene_interactive,
|
||||
set_pet_menu_state,
|
||||
start_google_auth,
|
||||
start_discord_auth,
|
||||
get_user_active_doll,
|
||||
get_scene_friends,
|
||||
login,
|
||||
register,
|
||||
change_password,
|
||||
reset_password,
|
||||
logout_and_restart,
|
||||
send_interaction_cmd,
|
||||
get_modules,
|
||||
set_scene_setup_nekos_position,
|
||||
set_scene_setup_nekos_opacity,
|
||||
set_scene_setup_nekos_scale
|
||||
get_presence_state
|
||||
])
|
||||
.events(collect_events![
|
||||
CursorMoved,
|
||||
SceneInteractiveChanged,
|
||||
AppDataRefreshed,
|
||||
AppStateChanged,
|
||||
NekoPositionsUpdated,
|
||||
ActiveDollSpriteChanged,
|
||||
SetInteractionOverlay,
|
||||
EditDoll,
|
||||
CreateDoll,
|
||||
UserStatusChanged,
|
||||
UserActiveDollUpdated,
|
||||
FriendCursorPositionUpdated,
|
||||
FriendDisconnected,
|
||||
FriendActiveDollChanged,
|
||||
FriendActiveDollSpritesUpdated,
|
||||
FriendUserStatusChanged,
|
||||
PresenceStateUpdated,
|
||||
SceneFriendsUpdated,
|
||||
InteractionReceived,
|
||||
InteractionDeliveryFailed,
|
||||
FriendRequestReceived,
|
||||
FriendRequestAccepted,
|
||||
FriendRequestDenied,
|
||||
Unfriended,
|
||||
AuthFlowUpdated
|
||||
Unfriended
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -136,7 +135,6 @@ pub fn run() {
|
||||
.expect("Failed to export TypeScript bindings");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_positioner::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,
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DollColorSchemeDto {
|
||||
pub outline: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DollConfigurationDto {
|
||||
pub color_scheme: DollColorSchemeDto,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateDollDto {
|
||||
pub name: String,
|
||||
pub configuration: Option<DollConfigurationDto>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateDollDto {
|
||||
pub name: Option<String>,
|
||||
pub configuration: Option<DollConfigurationDto>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DollDto {
|
||||
pub id: String,
|
||||
|
||||
@@ -5,34 +5,20 @@ use super::dolls::DollDto;
|
||||
use super::friends::UserBasicDto;
|
||||
use crate::services::presence_modules::models::PresenceStatus;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Type)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UserStatusState {
|
||||
Idle,
|
||||
Resting,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Type)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Type, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserStatusPayload {
|
||||
pub presence_status: PresenceStatus,
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FriendUserStatusPayload {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub mod app_data;
|
||||
pub mod app_state;
|
||||
pub mod dolls;
|
||||
pub mod event_payloads;
|
||||
pub mod friends;
|
||||
pub mod health;
|
||||
pub mod interaction;
|
||||
pub mod remote_error;
|
||||
pub mod scene;
|
||||
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 crate::{lock_r, services::auth::with_auth, state::FDOLL, models::dolls::*};
|
||||
use crate::models::remote_error::RemoteError;
|
||||
use crate::{lock_r, models::dolls::*, services::auth::with_auth, state::FDOLL};
|
||||
|
||||
pub struct DollsRemote {
|
||||
pub base_url: String,
|
||||
@@ -19,8 +19,7 @@ impl DollsRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.network
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{lock_r, services::auth::with_auth, state::FDOLL, models::friends::*};
|
||||
use crate::models::remote_error::RemoteError;
|
||||
use crate::{lock_r, models::friends::*, services::auth::with_auth, state::FDOLL};
|
||||
|
||||
pub struct FriendRemote {
|
||||
pub base_url: String,
|
||||
@@ -19,8 +19,7 @@ impl FriendRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.network
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.http_client
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{lock_r, models::health::*, state::FDOLL};
|
||||
use crate::{lock_r, state::FDOLL, models::health::*};
|
||||
|
||||
pub struct HealthRemote {
|
||||
pub base_url: String,
|
||||
@@ -18,8 +18,7 @@ impl HealthRemote {
|
||||
.ok_or(HealthError::ConfigMissing("api_base_url"))?;
|
||||
|
||||
let client = guard
|
||||
.network
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.map(|c| c.http_client.clone())
|
||||
.ok_or(HealthError::ConfigMissing("http_client"))?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 base_url: String,
|
||||
@@ -18,8 +18,7 @@ impl UserRemote {
|
||||
.expect("App configuration error")
|
||||
.clone(),
|
||||
client: guard
|
||||
.network
|
||||
.clients
|
||||
.network.clients
|
||||
.as_ref()
|
||||
.expect("App configuration error")
|
||||
.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::{
|
||||
models::{
|
||||
app_data::UserData,
|
||||
app_state::AppState,
|
||||
dolls::DollDto,
|
||||
event_payloads::{
|
||||
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
||||
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
||||
FriendUserStatusPayload, UnfriendedPayload, UserStatusPayload,
|
||||
},
|
||||
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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthFlowStatus {
|
||||
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)]
|
||||
#[tauri_specta(event_name = "cursor-position")]
|
||||
pub struct CursorMoved(pub CursorPositions);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "scene-interactive")]
|
||||
@@ -41,14 +32,6 @@ pub struct SceneInteractiveChanged(pub bool);
|
||||
#[tauri_specta(event_name = "app-data-refreshed")]
|
||||
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)]
|
||||
#[tauri_specta(event_name = "set-interaction-overlay")]
|
||||
pub struct SetInteractionOverlay(pub bool);
|
||||
@@ -66,8 +49,12 @@ pub struct CreateDoll;
|
||||
pub struct UserStatusChanged(pub UserStatusPayload);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "neko-positions")]
|
||||
pub struct NekoPositionsUpdated(pub NekoPositionsDto);
|
||||
#[tauri_specta(event_name = "user-active-doll-updated")]
|
||||
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)]
|
||||
#[tauri_specta(event_name = "friend-disconnected")]
|
||||
@@ -77,14 +64,18 @@ pub struct FriendDisconnected(pub FriendDisconnectedPayload);
|
||||
#[tauri_specta(event_name = "friend-active-doll-changed")]
|
||||
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)]
|
||||
#[tauri_specta(event_name = "friend-user-status")]
|
||||
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)]
|
||||
#[tauri_specta(event_name = "interaction-received")]
|
||||
pub struct InteractionReceived(pub InteractionPayloadDto);
|
||||
@@ -108,7 +99,3 @@ pub struct FriendRequestDenied(pub FriendRequestDeniedPayload);
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "unfriended")]
|
||||
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::{
|
||||
ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig,
|
||||
};
|
||||
use crate::get_app_handle;
|
||||
|
||||
pub static APP_MENU_WINDOW_LABEL: &str = "app_menu";
|
||||
|
||||
pub fn open_app_menu_window() {
|
||||
let mut config = WindowConfig::regular_ui(APP_MENU_WINDOW_LABEL, "/app-menu", "Friendolls");
|
||||
config.width = 400.0;
|
||||
config.height = 550.0;
|
||||
config.resizable = true;
|
||||
let app_handle = get_app_handle();
|
||||
let existing_webview_window = app_handle.get_window(APP_MENU_WINDOW_LABEL);
|
||||
|
||||
match ensure_window(&config, true, false) {
|
||||
Ok(EnsureWindowResult::Created(_)) => {}
|
||||
Ok(EnsureWindowResult::Existing(_)) => {}
|
||||
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
||||
error!(
|
||||
"Failed to build {} window due to missing parent '{}': impossible state",
|
||||
APP_MENU_WINDOW_LABEL, parent_label
|
||||
);
|
||||
if let Some(window) = existing_webview_window {
|
||||
window.show().unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
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(EnsureWindowError::ShowExisting(e))
|
||||
| Err(EnsureWindowError::SetParent(e))
|
||||
| Err(EnsureWindowError::Build(e)) => {
|
||||
Err(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 serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::Manager;
|
||||
use tracing::warn;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, warn};
|
||||
use url::Url;
|
||||
|
||||
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 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> {
|
||||
let dir = app_handle
|
||||
@@ -48,14 +71,12 @@ fn sanitize(mut config: AppConfig) -> AppConfig {
|
||||
.or_else(|| Some(DEFAULT_API_BASE_URL.to_string()))
|
||||
.map(|v| strip_trailing_slash(&v));
|
||||
|
||||
config.normalized()
|
||||
config
|
||||
}
|
||||
|
||||
pub fn default_app_config() -> AppConfig {
|
||||
AppConfig {
|
||||
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)
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
}
|
||||