fetch user profile (wip)

This commit is contained in:
2025-12-05 21:52:46 +08:00
parent a9068fe575
commit f522148489
18 changed files with 179 additions and 50 deletions

View File

@@ -1,7 +1,13 @@
# Tauri + SvelteKit + TypeScript
# Friendolls (Desktop)
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
Run the following command in project root on first run & after changes to models on Rust side to generate TypeScript type bindings from Rust models
## Recommended IDE Setup
```sh
# unix
TS_RS_EXPORT_DIR="../src/types/bindings" cargo test export_bindings --manifest-path=./src-tauri/Cargo.toml
```
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
```sh
# powershell
$Env:TS_RS_EXPORT_DIR = "../src/types/bindings"; cargo test export_bindings --manifest-path=./src-tauri/Cargo.toml
```

View File

@@ -1,4 +1,4 @@
use crate::services::cursor::start_cursor_tracking;
use crate::{models::app_data::AppData, services::cursor::start_cursor_tracking, state::FDOLL};
use tauri::async_runtime;
use tracing_subscriber;
@@ -6,6 +6,7 @@ static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync
mod app;
mod models;
mod remotes;
mod services;
mod state;
mod utilities;
@@ -44,13 +45,22 @@ fn register_app_events(event: tauri::RunEvent) {
}
}
#[tauri::command]
fn get_app_data() -> Result<AppData, String> {
let guard = lock_r!(FDOLL);
return Ok(guard.app_data.clone());
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![start_cursor_tracking])
.invoke_handler(tauri::generate_handler![
start_cursor_tracking,
get_app_data
])
.setup(|app| {
APP_HANDLE
.set(app.handle().to_owned())

View File

@@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct AuthConfig {
pub audience: String,
pub auth_url: String,
@@ -10,8 +8,7 @@ pub struct AuthConfig {
pub redirect_host: String,
}
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct AppConfig {
pub api_base_url: Option<String>,
pub auth: AuthConfig,

View File

@@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::remotes::user::UserProfile;
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
#[ts(export)]
pub struct AppData {
pub user: Option<UserProfile>,
}

View File

@@ -1 +1,2 @@
pub mod app_config;
pub mod app_data;

View File

@@ -0,0 +1 @@
pub mod user;

View File

@@ -0,0 +1,51 @@
use reqwest::{Client, Error};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::{lock_r, services::auth::with_auth, state::FDOLL};
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct UserProfile {
pub id: String,
pub name: String,
pub email: String,
pub username: String,
pub created_at: String,
pub last_login_at: String,
}
pub struct UserRemote {
pub base_url: String,
pub client: Client,
}
impl UserRemote {
pub fn new() -> Self {
let guard = lock_r!(FDOLL);
Self {
base_url: guard
.app_config
.api_base_url
.as_ref()
.expect("App configuration error")
.clone(),
client: guard
.clients
.as_ref()
.expect("App configuration error")
.http_client
.clone(),
}
}
pub async fn get_user(&self, user_id: Option<&str>) -> Result<UserProfile, Error> {
let url = format!("{}/users/{}", self.base_url, user_id.unwrap_or("me"));
let resp = with_auth(self.client.get(url)).await.send().await?;
let user = resp.json().await?;
Ok(user)
}
// TODO: Add other endpoints as methods
}

View File

@@ -6,18 +6,21 @@ use std::sync::Arc;
use std::time::Duration;
use tauri::Emitter;
use tracing::{error, info, warn};
use ts_rs::TS;
use crate::get_app_handle;
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CursorPosition {
pub x: i32,
pub y: i32,
}
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CursorPositions {
pub raw: CursorPosition,
pub mapped: CursorPosition,

View File

@@ -1,14 +1,20 @@
// in app-core/src/state.rs
use crate::{
lock_w,
models::app_config::{AppConfig, AuthConfig},
models::{
app_config::{AppConfig, AuthConfig},
app_data::AppData,
},
remotes::user::UserRemote,
services::auth::{load_auth_pass, AuthPass},
APP_HANDLE,
};
use serde_json::json;
use std::{
env,
sync::{Arc, LazyLock, RwLock},
};
use tauri::async_runtime;
use tauri::{async_runtime, Emitter};
use tracing::{info, warn};
#[derive(Default, Clone)]
@@ -29,6 +35,9 @@ pub struct AppState {
pub clients: Option<Clients>,
pub auth_pass: Option<AuthPass>,
pub oauth_flow: OAuthFlowTracker,
// exposed to the frontend
pub app_data: AppData,
}
// Global application state
@@ -81,8 +90,34 @@ pub fn init_fdoll_state() {
async_runtime::spawn(async move {
crate::services::ws::init_ws_client().await;
});
// TODO: seems like even under `has_auth` token may not be present when init app data
async_runtime::spawn(async move {
info!("Initializing user data");
init_app_data().await;
});
}
info!("Initialized FDOLL state (WebSocket client initializing asynchronously)");
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
}
}
/// To be called in init state or need to refresh data.
/// Populate user data in app state from the server.
pub async fn init_app_data() {
let user_remote = UserRemote::new();
let user = user_remote
.get_user(None)
.await
.expect("TODO: handle user profile fetch failure");
{
let mut guard = lock_w!(FDOLL);
guard.app_data.user = Some(user);
APP_HANDLE
.get()
// TODO: magic constants
.expect("App handle not initialized")
.emit("app-data-refreshed", json!(guard.app_data))
.expect("TODO: handle event emit fail");
}
}

View File

@@ -1,11 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { writable } from "svelte/store";
export type CursorPositions = {
raw: { x: number; y: number };
mapped: { x: number; y: number };
};
import type { CursorPositions } from "../types/bindings/CursorPositions";
export let cursorPositionOnScreen = writable<CursorPositions>({
raw: { x: 0, y: 0 },

View File

@@ -1,25 +1,24 @@
<script>
import { browser } from '$app/environment';
import { onMount, onDestroy } from 'svelte';
import { initCursorTracking, stopCursorTracking } from '../events/cursor';
import { browser } from "$app/environment";
import { onMount, onDestroy } from "svelte";
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
let { children } = $props();
if (browser) {
onMount(async () => {
try {
await initCursorTracking();
} catch (err) {
console.error("[Scene] Failed to initialize cursor tracking:", err);
}
});
onDestroy(() => {
stopCursorTracking();
});
}
let { children } = $props();
if (browser) {
onMount(async () => {
try {
await initCursorTracking();
} catch (err) {
console.error("[Scene] Failed to initialize cursor tracking:", err);
}
});
onDestroy(() => {
stopCursorTracking();
});
}
</script>
<div class="size-full bg-transparent">
{@render children?.()}
{@render children?.()}
</div>

View File

@@ -1,3 +1,3 @@
<main class="card-body">
<button class="btn btn-primary">Hello TailwindCSS!</button>
<button class="btn btn-primary">Hello TailwindCSS!</button>
</main>

View File

@@ -1,9 +1,6 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import {
cursorPositionOnScreen,
} from "../../events/cursor";
import { cursorPositionOnScreen } from "../../events/cursor";
</script>
<div class="p-4">
@@ -17,12 +14,22 @@
<h3 class="font-semibold mb-2">Cursor Position (Multi-Window Test)</h3>
<div class="flex flex-col gap-1">
<span class="font-mono text-sm">
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y})
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw
.y})
</span>
<span class="font-mono text-sm">
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen.mapped.y})
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
.mapped.y})
</span>
</div>
</div>
<button
class="btn"
onclick={() => {
invoke("get_app_data").then((data) => {
console.log("data", data);
});
}}>Fetch app data</button
>
</div>
</div>

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import {
cursorPositionOnScreen,
} from "../../events/cursor";
import { cursorPositionOnScreen } from "../../events/cursor";
</script>
<div class="w-svw h-svh p-4 relative">
@@ -13,7 +11,8 @@
<p class="text-sm opacity-50">Scene Screen</p>
<div class="mt-4 flex flex-col gap-1">
<span class="font-mono text-sm">
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y})
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw
.y})
</span>
<span class="font-mono text-sm">
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserProfile } from "./UserProfile";
export type AppData = { user: UserProfile | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CursorPosition = { x: number, y: number, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CursorPosition } from "./CursorPosition";
export type CursorPositions = { raw: CursorPosition, mapped: CursorPosition, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UserProfile = { id: string, name: string, email: string, username: string, createdAt: string, lastLoginAt: string, };