From 8b144d87f4a6bd79de633f430cedd7c4cdf72ea2 Mon Sep 17 00:00:00 2001 From: eiiko6 Date: Sun, 11 Jan 2026 18:04:24 +0100 Subject: [PATCH] fullstack: added profile pictures/avatars to accounts and improved settings page layout --- package.json | 2 + src-tauri/Cargo.lock | 106 +++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 5 +- src-tauri/src/lib.rs | 2 + src-tauri/tauri.conf.json | 11 +- src/api/account.ts | 31 ++- src/api/client.ts | 5 +- src/authStore.ts | 16 +- src/components/UpdateAccountModal.vue | 5 +- src/components/UploadAvatarModal.vue | 270 ++++++++++++++++++++++++++ src/locales/en.ftl | 6 + src/locales/fr.ftl | 8 +- src/pages/SettingsPage.vue | 221 +++++++++++++-------- src/store.ts | 4 +- src/types.ts | 1 + yarn.lock | 14 ++ 17 files changed, 608 insertions(+), 103 deletions(-) create mode 100644 src/components/UploadAvatarModal.vue diff --git a/package.json b/package.json index c73087a..531dcc2 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "dependencies": { "@fluent/bundle": "^0.19.1", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-store": "~2", + "@tauri-apps/plugin-upload": "~2", "@tauri-apps/plugin-websocket": "~2", "fluent-vue": "^3.8.1", "vue": "^3.5.13", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6fa6614..fadca11 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -443,9 +443,11 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-http", "tauri-plugin-opener", "tauri-plugin-store", + "tauri-plugin-upload", "tauri-plugin-websocket", ] @@ -759,6 +761,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", + "block2", + "libc", "objc2", ] @@ -1038,6 +1042,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1045,6 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1112,6 +1132,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1527,6 +1548,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -3024,6 +3051,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "read-progress-stream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6435842fc2fea44b528719eb8c32203bbc1bb2f5b619fbe0c0a3d8350fd8d2a8" +dependencies = [ + "bytes", + "futures", + "pin-project-lite", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3139,6 +3177,30 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -3827,6 +3889,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", @@ -3942,10 +4005,28 @@ dependencies = [ ] [[package]] -name = "tauri-plugin-fs" -version = "2.4.4" +name = "tauri-plugin-dialog" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +checksum = "05416b57601eca8666b5ec4186f5b1dc826ed35263b4797ad6641e58da6bc6c3" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" dependencies = [ "anyhow", "dunce", @@ -4025,6 +4106,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tauri-plugin-upload" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2844560c33b360506cea7289626267f43be253a01018b53bd556e86163b9fd" +dependencies = [ + "futures-util", + "log", + "read-progress-stream", + "reqwest", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", + "tokio-util", +] + [[package]] name = "tauri-plugin-websocket" version = "2.4.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c34e21d..8c68dee 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,11 +18,13 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["devtools"] } +tauri = { version = "2", features = ["protocol-asset", "devtools"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-store = "2" tauri-plugin-http = "2" tauri-plugin-websocket = "2" +tauri-plugin-upload = "2" +tauri-plugin-dialog = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c1d4158..b71eae2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -32,6 +32,9 @@ } ] }, - "websocket:default" + "websocket:default", + "upload:default", + "dialog:default" ] } + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2e908d..2ce3288 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ fn greet(name: &str) -> String { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_websocket::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_store::Builder::new().build()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5cf8779..e843125 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,11 +19,18 @@ "resizable": true, "fullscreen": false, "center": true, - "theme": "Dark" + "theme": "Dark", + "dragDropEnabled": true } ], "security": { - "csp": null + "csp": null, + "assetProtocol": { + "enable": true, + "scope": [ + "**" + ] + } } }, "bundle": { diff --git a/src/api/account.ts b/src/api/account.ts index cf83b37..ff78153 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -1,10 +1,37 @@ import { UpdateUserResponse } from '../types'; import { apiFetch } from './client' -// import type { User } from '../types' +import { upload } from '@tauri-apps/plugin-upload'; +import { getAuthData } from '../authStore'; +import { API } from '../main.ts'; export function updateSettings(username: string, email: string, password: string) { - return apiFetch('/settings', { + return apiFetch('/account/settings', { method: 'PUT', body: JSON.stringify({ username, email, password }), }); } + +export async function uploadAvatar( + filePath: string, + onProgress: (progress: number, total: number) => void +) { + const auth = await getAuthData(); + const url = `${API}/account/upload-avatar`; + const headers = new Map([ + ['Authorization', `Bearer ${auth.token}`], + ['Content-Type', 'application/octet-stream'] + ]); + + return upload( + url, + filePath, + ({ progress, total }) => { + onProgress(progress, total); + }, + headers + ); +} + +export function getAvatar(uuid: string): string { + return `${API}/account/get-avatar/${uuid}`; +} diff --git a/src/api/client.ts b/src/api/client.ts index af5a4c1..6c7c2a6 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -9,11 +9,14 @@ export async function apiFetch( ): Promise { const auth = await getAuthData() + const isFormData = options.body instanceof FormData; + const res = await fetch(`${API}${path}`, { ...options, method: options.method || 'GET', headers: { - 'Content-Type': 'application/json', + // Only add json header if it's not formdata + ...(!isFormData ? { 'Content-Type': 'application/json' } : {}), ...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}), ...options.headers, }, diff --git a/src/authStore.ts b/src/authStore.ts index 9af9894..2084f77 100644 --- a/src/authStore.ts +++ b/src/authStore.ts @@ -1,5 +1,6 @@ import { load, Store } from '@tauri-apps/plugin-store' import { UpdateUserResponse, User } from './types' +import { getAvatar } from './api/account' let store: Store | null = null @@ -32,7 +33,8 @@ export async function updateLocalUser(newData: UpdateUserResponse, uuid?: string const updatedUser = { username: newData.username, email: newData.email, - uuid + uuid, + avatar_url: `${getAvatar(uuid || '')}?t=${Date.now()}` } const s = await getStore() @@ -40,6 +42,18 @@ export async function updateLocalUser(newData: UpdateUserResponse, uuid?: string await s.save() } +export async function refreshLocalUser() { + const s = await getStore() + const user = await s.get('user') + + if (user) { + user.avatar_url = `${getAvatar(user.uuid)}?t=${Date.now()}` + + await s.set('user', user) + await s.save() + } +} + export async function setLastRoom(uuid: string) { if (!uuid || uuid === 'none') return const s = await getStore() diff --git a/src/components/UpdateAccountModal.vue b/src/components/UpdateAccountModal.vue index abb325c..0ec692e 100644 --- a/src/components/UpdateAccountModal.vue +++ b/src/components/UpdateAccountModal.vue @@ -28,7 +28,7 @@ @@ -95,8 +95,7 @@ async function submit() { .backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); + background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; diff --git a/src/components/UploadAvatarModal.vue b/src/components/UploadAvatarModal.vue new file mode 100644 index 0000000..c10691f --- /dev/null +++ b/src/components/UploadAvatarModal.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/src/locales/en.ftl b/src/locales/en.ftl index aee4c8a..5f8afc5 100644 --- a/src/locales/en.ftl +++ b/src/locales/en.ftl @@ -62,6 +62,7 @@ notifications-error-room-accept = An error occurred while accepting the invite. notifications-error-room-decline = An error occurred while declining the invite. ## Settings page +settings-loading = Loading settings... settings-title = Settings settings-account = Account settings-language = Language @@ -74,6 +75,9 @@ settings-account-update-modal-title = Update your Account settings-account-update-subtitle = Only fill in the fields you wish to change. settings-new-password = New Password settings-new-password-confirm = Confirm new password +settings-upload-prompt = Upload an image +settings-upload-avatar-btn = Upload an avatar +settings-upload-avatar-title = Upload an avatar settings-update-save = Save Changes settings-updating = Updating... settings-error-required = Username and Email are required. @@ -82,3 +86,5 @@ settings-error-failed = Update failed ## Shared shared-cancel = Cancel shared-error = An error occurred +shared-save = Save +shared-updating = Updating diff --git a/src/locales/fr.ftl b/src/locales/fr.ftl index 52b81af..f493455 100644 --- a/src/locales/fr.ftl +++ b/src/locales/fr.ftl @@ -62,6 +62,7 @@ notifications-error-room-accept = Erreur lors de l'acceptation de l'invitation. notifications-error-room-decline = Erreur lors du refus de l'invitation. ## Settings page +settings-loading = Chargement des paramètres... settings-title = Paramètres settings-account = Compte settings-language = Langue @@ -74,11 +75,14 @@ settings-account-update-modal-title = Modifier votre compte settings-account-update-subtitle = Remplissez uniquement ce que vous souhaitez changer. settings-new-password = Nouveau mot de passe settings-new-password-confirm = Confirmer le mot de passe -settings-update-save = Enregistrer -settings-updating = Mise à jour... +settings-upload-prompt = Importer une image +settings-upload-avatar-btn = Importer un avatar +settings-upload-avatar-title = Importer un avatar settings-error-required = Le nom d'utilisateur et l'email sont requis. settings-error-failed = Échec de la mise à jour ## Shared shared-cancel = Annuler shared-error = Une erreur est survenue +shared-save = Enregistrer +shared-updating = Mise à jour... diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index 4f5040b..1484843 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -1,35 +1,48 @@ diff --git a/src/store.ts b/src/store.ts index 16f28f7..226eced 100644 --- a/src/store.ts +++ b/src/store.ts @@ -5,6 +5,7 @@ import { ref, computed } from 'vue' import { fetchFriendRequests } from './api/friends' import { fetchRoomInvites } from './api/rooms' import type { FriendRequest, RoomInvite } from './types' +import { getAvatar } from './api/account' export const initAuth = authStore.getAuthData export const getLastRoom = authStore.getLastRoom @@ -19,7 +20,8 @@ export async function login(email: string, username: string, password: string) { let user: User = { uuid: res.uuid, username: res.username, - email: res.email + email: res.email, + avatar_url: await getAvatar(res.uuid) }; await authStore.saveAuthData(res.token, user) return { token: res.token, uuid: res.uuid, isAuthenticated: true } diff --git a/src/types.ts b/src/types.ts index a27b14c..18e71f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ export interface User { uuid: string username: string email: string + avatar_url: string } export interface LoginResponse { diff --git a/yarn.lock b/yarn.lock index 584da4f..e0067bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -402,6 +402,13 @@ "@tauri-apps/cli-win32-ia32-msvc" "2.9.6" "@tauri-apps/cli-win32-x64-msvc" "2.9.6" +"@tauri-apps/plugin-dialog@~2": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.5.0.tgz#52057077b52cc51643ac9829d48c2c590e5e1a54" + integrity sha512-I0R0ygwRd9AN8Wj5GnzCogOlqu2+OWAtBd0zEC4+kQCI32fRowIyuhPCBoUv4h/lQt2bM39kHlxPHD5vDcFjiA== + dependencies: + "@tauri-apps/api" "^2.8.0" + "@tauri-apps/plugin-http@~2": version "2.5.4" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.5.4.tgz#998a9cd02efa006fcbeddd92e8e51434ff3804dd" @@ -423,6 +430,13 @@ dependencies: "@tauri-apps/api" "^2.8.0" +"@tauri-apps/plugin-upload@~2": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-upload/-/plugin-upload-2.4.0.tgz#a08f7174471936429f5601c1477f40462915b2e8" + integrity sha512-ebhsqXmiELnpKu2p46EZG14UKxvbVP28BpJBiHzR+quWVrMxm40518PXTDlXXcJUW5CkbmP/6RL5ERSVXBL8sQ== + dependencies: + "@tauri-apps/api" "^2.8.0" + "@tauri-apps/plugin-websocket@~2": version "2.4.1" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-websocket/-/plugin-websocket-2.4.1.tgz#90774d5be337f92fc2a770cc0d98a0fbaea107fc"