fetch user profile (wip)
This commit is contained in:
14
README.md
14
README.md
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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 tauri::async_runtime;
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync
|
|||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod remotes;
|
||||||
mod services;
|
mod services;
|
||||||
mod state;
|
mod state;
|
||||||
mod utilities;
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_positioner::init())
|
.plugin(tauri_plugin_positioner::init())
|
||||||
.plugin(tauri_plugin_opener::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| {
|
.setup(|app| {
|
||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
.set(app.handle().to_owned())
|
.set(app.handle().to_owned())
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
#[ts(export)]
|
|
||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
pub audience: String,
|
pub audience: String,
|
||||||
pub auth_url: String,
|
pub auth_url: String,
|
||||||
@@ -10,8 +8,7 @@ pub struct AuthConfig {
|
|||||||
pub redirect_host: String,
|
pub redirect_host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
#[ts(export)]
|
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub api_base_url: Option<String>,
|
pub api_base_url: Option<String>,
|
||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
|
|||||||
10
src-tauri/src/models/app_data.rs
Normal file
10
src-tauri/src/models/app_data.rs
Normal 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>,
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod app_config;
|
pub mod app_config;
|
||||||
|
pub mod app_data;
|
||||||
|
|||||||
1
src-tauri/src/remotes/mod.rs
Normal file
1
src-tauri/src/remotes/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod user;
|
||||||
51
src-tauri/src/remotes/user.rs
Normal file
51
src-tauri/src/remotes/user.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -6,18 +6,21 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct CursorPosition {
|
pub struct CursorPosition {
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub y: i32,
|
pub y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
pub struct CursorPositions {
|
pub struct CursorPositions {
|
||||||
pub raw: CursorPosition,
|
pub raw: CursorPosition,
|
||||||
pub mapped: CursorPosition,
|
pub mapped: CursorPosition,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
lock_w,
|
lock_w,
|
||||||
models::app_config::{AppConfig, AuthConfig},
|
models::{
|
||||||
|
app_config::{AppConfig, AuthConfig},
|
||||||
|
app_data::AppData,
|
||||||
|
},
|
||||||
|
remotes::user::UserRemote,
|
||||||
services::auth::{load_auth_pass, AuthPass},
|
services::auth::{load_auth_pass, AuthPass},
|
||||||
|
APP_HANDLE,
|
||||||
};
|
};
|
||||||
|
use serde_json::json;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
sync::{Arc, LazyLock, RwLock},
|
sync::{Arc, LazyLock, RwLock},
|
||||||
};
|
};
|
||||||
use tauri::async_runtime;
|
use tauri::{async_runtime, Emitter};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -29,6 +35,9 @@ pub struct AppState {
|
|||||||
pub clients: Option<Clients>,
|
pub clients: Option<Clients>,
|
||||||
pub auth_pass: Option<AuthPass>,
|
pub auth_pass: Option<AuthPass>,
|
||||||
pub oauth_flow: OAuthFlowTracker,
|
pub oauth_flow: OAuthFlowTracker,
|
||||||
|
|
||||||
|
// exposed to the frontend
|
||||||
|
pub app_data: AppData,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global application state
|
// Global application state
|
||||||
@@ -81,8 +90,34 @@ pub fn init_fdoll_state() {
|
|||||||
async_runtime::spawn(async move {
|
async_runtime::spawn(async move {
|
||||||
crate::services::ws::init_ws_client().await;
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import type { CursorPositions } from "../types/bindings/CursorPositions";
|
||||||
export type CursorPositions = {
|
|
||||||
raw: { x: number; y: number };
|
|
||||||
mapped: { x: number; y: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export let cursorPositionOnScreen = writable<CursorPositions>({
|
export let cursorPositionOnScreen = writable<CursorPositions>({
|
||||||
raw: { x: 0, y: 0 },
|
raw: { x: 0, y: 0 },
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import { browser } from '$app/environment';
|
import { browser } from "$app/environment";
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { initCursorTracking, stopCursorTracking } from '../events/cursor';
|
import { initCursorTracking, stopCursorTracking } from "../events/cursor";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
if (browser) {
|
if (browser) {
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await initCursorTracking();
|
await initCursorTracking();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Scene] Failed to initialize cursor tracking:", err);
|
console.error("[Scene] Failed to initialize cursor tracking:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
stopCursorTracking();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
stopCursorTracking();
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="size-full bg-transparent">
|
<div class="size-full bg-transparent">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<main class="card-body">
|
<main class="card-body">
|
||||||
<button class="btn btn-primary">Hello TailwindCSS!</button>
|
<button class="btn btn-primary">Hello TailwindCSS!</button>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import {
|
import { cursorPositionOnScreen } from "../../events/cursor";
|
||||||
cursorPositionOnScreen,
|
|
||||||
} from "../../events/cursor";
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@@ -17,12 +14,22 @@
|
|||||||
<h3 class="font-semibold mb-2">Cursor Position (Multi-Window Test)</h3>
|
<h3 class="font-semibold mb-2">Cursor Position (Multi-Window Test)</h3>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y})
|
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw
|
||||||
|
.y})
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen.mapped.y})
|
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
|
||||||
|
.mapped.y})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
onclick={() => {
|
||||||
|
invoke("get_app_data").then((data) => {
|
||||||
|
console.log("data", data);
|
||||||
|
});
|
||||||
|
}}>Fetch app data</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { cursorPositionOnScreen } from "../../events/cursor";
|
||||||
cursorPositionOnScreen,
|
|
||||||
} from "../../events/cursor";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-svw h-svh p-4 relative">
|
<div class="w-svw h-svh p-4 relative">
|
||||||
@@ -13,7 +11,8 @@
|
|||||||
<p class="text-sm opacity-50">Scene Screen</p>
|
<p class="text-sm opacity-50">Scene Screen</p>
|
||||||
<div class="mt-4 flex flex-col gap-1">
|
<div class="mt-4 flex flex-col gap-1">
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y})
|
Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw
|
||||||
|
.y})
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono text-sm">
|
<span class="font-mono text-sm">
|
||||||
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
|
Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen
|
||||||
|
|||||||
4
src/types/bindings/AppData.ts
Normal file
4
src/types/bindings/AppData.ts
Normal 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, };
|
||||||
3
src/types/bindings/CursorPosition.ts
Normal file
3
src/types/bindings/CursorPosition.ts
Normal 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, };
|
||||||
4
src/types/bindings/CursorPositions.ts
Normal file
4
src/types/bindings/CursorPositions.ts
Normal 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, };
|
||||||
3
src/types/bindings/UserProfile.ts
Normal file
3
src/types/bindings/UserProfile.ts
Normal 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, };
|
||||||
Reference in New Issue
Block a user