fixed and improved profile pictures
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"@fluent/bundle": "^0.19.1",
|
"@fluent/bundle": "^0.19.1",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
|
"@tauri-apps/plugin-fs": "~2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-store": "~2",
|
"@tauri-apps/plugin-store": "~2",
|
||||||
|
|||||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -444,6 +444,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ tauri-plugin-http = "2"
|
|||||||
tauri-plugin-websocket = "2"
|
tauri-plugin-websocket = "2"
|
||||||
tauri-plugin-upload = "2"
|
tauri-plugin-upload = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-fs = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
{
|
{
|
||||||
"url": "http://127.0.0.1:*"
|
"url": "http://127.0.0.1:*"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "http://192.168.1.183:*"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://alatreon.org/chatapp/*"
|
"url": "https://alatreon.org/chatapp/*"
|
||||||
}
|
}
|
||||||
@@ -27,6 +30,9 @@
|
|||||||
{
|
{
|
||||||
"url": "ws://127.0.0.1:*"
|
"url": "ws://127.0.0.1:*"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "http://192.168.1.183:*"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "wss://alatreon.org/chatapp/*"
|
"url": "wss://alatreon.org/chatapp/*"
|
||||||
}
|
}
|
||||||
@@ -34,7 +40,14 @@
|
|||||||
},
|
},
|
||||||
"websocket:default",
|
"websocket:default",
|
||||||
"upload:default",
|
"upload:default",
|
||||||
"dialog:default"
|
"dialog:default",
|
||||||
|
"fs:default",
|
||||||
|
{
|
||||||
|
"identifier": "fs:scope",
|
||||||
|
"allow": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<!-- AndroidTV support -->
|
<!-- AndroidTV support -->
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
|
|||||||
#[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_fs::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_upload::init())
|
.plugin(tauri_plugin_upload::init())
|
||||||
.plugin(tauri_plugin_websocket::init())
|
.plugin(tauri_plugin_websocket::init())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { UpdateUserResponse } from '../types';
|
import { UpdateUserResponse } from '../types';
|
||||||
import { apiFetch } from './client'
|
import { apiFetch } from './client'
|
||||||
import { upload } from '@tauri-apps/plugin-upload';
|
// import { upload } from '@tauri-apps/plugin-upload';
|
||||||
import { getAuthData } from '../authStore';
|
import { getAuthData } from '../store';
|
||||||
import { API } from '../main.ts';
|
import { API } from '../main.ts';
|
||||||
|
|
||||||
export function updateSettings(username: string, email: string, password: string) {
|
export function updateSettings(username: string, email: string, password: string) {
|
||||||
@@ -12,24 +12,41 @@ export function updateSettings(username: string, email: string, password: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadAvatar(
|
export async function uploadAvatar(
|
||||||
filePath: string,
|
fileData: Uint8Array,
|
||||||
onProgress: (progress: number, total: number) => void
|
onProgress: (progress: number, total: number) => void
|
||||||
) {
|
) {
|
||||||
const auth = await getAuthData();
|
const auth = await getAuthData();
|
||||||
const url = `${API}/account/upload-avatar`;
|
const url = `${API}/account/upload-avatar`;
|
||||||
const headers = new Map<string, string>([
|
|
||||||
['Authorization', `Bearer ${auth.token}`],
|
|
||||||
['Content-Type', 'application/octet-stream']
|
|
||||||
]);
|
|
||||||
|
|
||||||
return upload(
|
return new Promise((resolve, reject) => {
|
||||||
url,
|
const xhr = new XMLHttpRequest();
|
||||||
filePath,
|
xhr.open('POST', url);
|
||||||
({ progress, total }) => {
|
|
||||||
onProgress(progress, total);
|
xhr.setRequestHeader('Authorization', `Bearer ${auth.token}`);
|
||||||
},
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||||
headers
|
|
||||||
);
|
// Handle Progress
|
||||||
|
if (xhr.upload && onProgress) {
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
onProgress(event.loaded, event.total);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Response
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(xhr.response);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.responseText}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new Error('Network error during upload'));
|
||||||
|
|
||||||
|
xhr.send(fileData);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvatar(uuid: string): string {
|
export function getAvatar(uuid: string): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetch } from '@tauri-apps/plugin-http';
|
import { fetch } from '@tauri-apps/plugin-http';
|
||||||
import { getAuthData, clearAuthData } from '../authStore'
|
import { getAuthData, clearAuthData } from '../store'
|
||||||
import { API } from '../main.ts'
|
import { API } from '../main.ts'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { load, Store } from '@tauri-apps/plugin-store'
|
|
||||||
import { UpdateUserResponse, User } from './types'
|
|
||||||
import { getAvatar } from './api/account'
|
|
||||||
|
|
||||||
let store: Store | null = null
|
|
||||||
|
|
||||||
async function getStore() {
|
|
||||||
if (!store) store = await load('store.json')
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAuthData() {
|
|
||||||
const s = await getStore()
|
|
||||||
const token = await s.get<string>('token')
|
|
||||||
const user = await s.get<User>('user')
|
|
||||||
return { token: token || null, user: user || null, isAuthenticated: !!token }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveAuthData(token: string, user: User) {
|
|
||||||
const s = await getStore()
|
|
||||||
await s.set('token', token)
|
|
||||||
await s.set('user', user)
|
|
||||||
await s.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clearAuthData() {
|
|
||||||
const s = await getStore()
|
|
||||||
s.clear()
|
|
||||||
await s.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateLocalUser(newData: UpdateUserResponse, uuid?: string) {
|
|
||||||
const updatedUser = {
|
|
||||||
username: newData.username,
|
|
||||||
email: newData.email,
|
|
||||||
uuid,
|
|
||||||
avatar_url: `${getAvatar(uuid || '')}?t=${Date.now()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const s = await getStore()
|
|
||||||
await s.set('user', updatedUser)
|
|
||||||
await s.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshLocalUser() {
|
|
||||||
const s = await getStore()
|
|
||||||
const user = await s.get<User>('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()
|
|
||||||
await s.set('last_room_uuid', uuid)
|
|
||||||
await s.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLastRoom(): Promise<string | null> {
|
|
||||||
const s = await getStore()
|
|
||||||
return (await s.get<string>('last_room_uuid')) ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveLocalePreference(locale: string) {
|
|
||||||
const s = await getStore()
|
|
||||||
await s.set('language', locale)
|
|
||||||
await s.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLocalePreference(): Promise<string | null> {
|
|
||||||
const s = await getStore()
|
|
||||||
return (await s.get<string>('language')) ?? null
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,7 @@ import MessageList from "./MessageList.vue";
|
|||||||
import MessageInput from "./MessageInput.vue";
|
import MessageInput from "./MessageInput.vue";
|
||||||
import InvitePeopleModal from './InvitePeopleModal.vue';
|
import InvitePeopleModal from './InvitePeopleModal.vue';
|
||||||
import WebSocket from '@tauri-apps/plugin-websocket';
|
import WebSocket from '@tauri-apps/plugin-websocket';
|
||||||
import { getAuthData } from "../authStore.ts";
|
import { getAuthData } from "../store.ts";
|
||||||
import { fetchRoomInfo } from "../api/rooms.ts";
|
import { fetchRoomInfo } from "../api/rooms.ts";
|
||||||
|
|
||||||
const props = defineProps<{ uuid: string }>();
|
const props = defineProps<{ uuid: string }>();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
|
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
|
||||||
<div class="sender-info">
|
<div class="sender-info">
|
||||||
<img :src="getAvatar(m.sender_uuid)" @error="handleAvatarError" class="sender-avatar" />
|
<img :src="getAvatarUrl(m.sender_uuid)" @error="handleAvatarError" class="sender-avatar" />
|
||||||
<div class="sender">{{ m.sender }}</div>
|
<div class="sender">{{ m.sender }}</div>
|
||||||
<span class="timestamp">{{ m.sent_at }}</span>
|
<span class="timestamp">{{ m.sent_at }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,13 +11,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import type { Message } from '../types'
|
import type { Message } from '../types'
|
||||||
import { getAvatar } from '../api/account.ts'
|
import { getAvatarUrl } from '../store.ts'
|
||||||
import defaultAvatar from '../assets/default-avatar.png'
|
import defaultAvatar from '../assets/default-avatar.png'
|
||||||
import { getAuthData } from '../authStore';
|
import { getAuthData } from '../store.ts';
|
||||||
|
|
||||||
defineProps<{ messages: Message[] }>()
|
defineProps<{ messages: Message[] }>()
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ ul {
|
|||||||
/* border-radius: var(--radius) var(--radius) 0 0; */
|
/* border-radius: var(--radius) var(--radius) 0 0; */
|
||||||
/* background-color: rgba(255, 255, 255, 0.02); */
|
/* background-color: rgba(255, 255, 255, 0.02); */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px 10px;
|
padding: 5px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-avatar {
|
.sender-avatar {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { updateSettings } from '../api/account'
|
import { updateSettings } from '../api/account'
|
||||||
import { updateLocalUser } from '../authStore'
|
import { updateLocalUser } from '../store'
|
||||||
import type { User } from '../types'
|
import type { User } from '../types'
|
||||||
import { useFluent } from 'fluent-vue';
|
import { useFluent } from 'fluent-vue';
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,11 @@ import { ref, onMounted, onUnmounted } from 'vue';
|
|||||||
import { uploadAvatar } from '../api/account';
|
import { uploadAvatar } from '../api/account';
|
||||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
// import { convertFileSrc } from '@tauri-apps/api/core';
|
||||||
import { refreshLocalUser } from '../authStore';
|
import { readFile } from '@tauri-apps/plugin-fs';
|
||||||
|
import { refreshLocalUser } from '../store.ts';
|
||||||
|
import { getAuthData } from '../store.ts';
|
||||||
|
import { refreshAvatar } from '../store.ts';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'updated']);
|
const emit = defineEmits(['close', 'updated']);
|
||||||
|
|
||||||
@@ -59,24 +62,13 @@ let unlistenHover: UnlistenFn;
|
|||||||
let unlistenLeave: UnlistenFn;
|
let unlistenLeave: UnlistenFn;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// When files are hovered over the window
|
unlistenHover = await listen('tauri://drag-enter', () => isDragging.value = true);
|
||||||
unlistenHover = await listen('tauri://drag-enter', () => {
|
unlistenLeave = await listen('tauri://drag-leave', () => isDragging.value = false);
|
||||||
isDragging.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
unlistenLeave = await listen('tauri://drag-leave', () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// When files are dropped
|
|
||||||
unlistenDrag = await listen<{ paths: string[] }>('tauri://drag-drop', (event) => {
|
unlistenDrag = await listen<{ paths: string[] }>('tauri://drag-drop', (event) => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
const path = event.payload.paths[0];
|
const path = event.payload.paths[0];
|
||||||
if (path && isImage(path)) {
|
if (path && isImage(path)) setFile(path);
|
||||||
setFile(path);
|
else errorMessage.value = "Please drop a valid image file.";
|
||||||
} else {
|
|
||||||
errorMessage.value = "Please drop a valid image file.";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,6 +76,8 @@ onUnmounted(() => {
|
|||||||
if (unlistenDrag) unlistenDrag();
|
if (unlistenDrag) unlistenDrag();
|
||||||
if (unlistenHover) unlistenHover();
|
if (unlistenHover) unlistenHover();
|
||||||
if (unlistenLeave) unlistenLeave();
|
if (unlistenLeave) unlistenLeave();
|
||||||
|
// Clean up
|
||||||
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function pickFile() {
|
async function pickFile() {
|
||||||
@@ -94,18 +88,30 @@ async function pickFile() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (selected && typeof selected === 'string') {
|
if (selected && typeof selected === 'string') {
|
||||||
setFile(selected);
|
await setFile(selected);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility to set path and preview
|
// Async function to read file and create blob URL
|
||||||
function setFile(path: string) {
|
async function setFile(path: string) {
|
||||||
|
try {
|
||||||
selectedPath.value = path;
|
selectedPath.value = path;
|
||||||
previewUrl.value = convertFileSrc(path);
|
|
||||||
|
const contents = await readFile(path);
|
||||||
|
const blob = new Blob([contents]);
|
||||||
|
|
||||||
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value);
|
||||||
|
|
||||||
|
previewUrl.value = URL.createObjectURL(blob);
|
||||||
|
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error reading file:", e);
|
||||||
|
errorMessage.value = "Could not read image file.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImage(path: string) {
|
function isImage(path: string) {
|
||||||
@@ -120,17 +126,25 @@ async function handleUpload() {
|
|||||||
uploadProgress.value = 0;
|
uploadProgress.value = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await uploadAvatar(selectedPath.value, (progress, total) => {
|
const fileBytes = await readFile(selectedPath.value);
|
||||||
|
|
||||||
|
await uploadAvatar(fileBytes, (progress, total) => {
|
||||||
uploadProgress.value = Math.round((progress / total) * 100);
|
uploadProgress.value = Math.round((progress / total) * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
await refreshLocalUser();
|
await refreshLocalUser();
|
||||||
|
|
||||||
|
// Trigger global UI refresh
|
||||||
|
const auth = await getAuthData();
|
||||||
|
if (auth.user) {
|
||||||
|
refreshAvatar(auth.user.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
emit('updated');
|
emit('updated');
|
||||||
emit('close');
|
emit('close');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Upload failed:", err);
|
console.error("Upload failed:", err);
|
||||||
errorMessage.value = 'Failed to upload avatar. Please try again.';
|
errorMessage.value = err.message || 'Failed to upload avatar';
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +268,8 @@ async function handleUpload() {
|
|||||||
.error-message {
|
.error-message {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { validateToken } from './store.ts'
|
|||||||
import { fluent, setLanguage } from './i18n'
|
import { fluent, setLanguage } from './i18n'
|
||||||
|
|
||||||
import './base.css'
|
import './base.css'
|
||||||
import { getLocalePreference } from './authStore.ts'
|
import { getLocalePreference } from './store.ts'
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await validateToken()
|
await validateToken()
|
||||||
@@ -28,7 +28,9 @@ async function init() {
|
|||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
export const API = 'http://127.0.0.1:8080'
|
// export const API = 'http://127.0.0.1:8080'
|
||||||
|
export const API = 'http://192.168.1.183:8080'
|
||||||
// export const API = 'https://alatreon.org/chatapp'
|
// export const API = 'https://alatreon.org/chatapp'
|
||||||
export const API_WS = 'ws://127.0.0.1:8080/ws'
|
// export const API_WS = 'ws://127.0.0.1:8080/ws'
|
||||||
|
export const API_WS = 'ws://192.168.1.183:8080/ws'
|
||||||
// export const API_WS = 'wss://alatreon.org/chatapp/ws'
|
// export const API_WS = 'wss://alatreon.org/chatapp/ws'
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<h1>{{ $t('settings-title') }}</h1>
|
<h1>{{ $t('settings-title') }}</h1>
|
||||||
|
|
||||||
<UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false" @updated="fetchUserData" />
|
<UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false" @updated="fetchUserData" />
|
||||||
<UploadAvatarModal v-if="showAvatarModal" @close="showAvatarModal = false" @updated="fetchUserData" />
|
<UploadAvatarModal v-if="showAvatarModal" @close="showAvatarModal = false" />
|
||||||
|
|
||||||
<h2>{{ $t('settings-account') }}</h2>
|
<h2>{{ $t('settings-account') }}</h2>
|
||||||
<div v-if="user" class="info-card">
|
<div v-if="user" class="info-card">
|
||||||
<div class="avatar-display">
|
<div class="avatar-display">
|
||||||
<img :src="getAvatar(user.uuid)" @error="handleAvatarError" class="avatar-img" />
|
<img :src="getAvatarUrl(user.uuid)" @error="handleAvatarError" class="avatar-img" />
|
||||||
|
|
||||||
<button class="update-btn" @click="showAvatarModal = true">
|
<button class="update-btn" @click="showAvatarModal = true">
|
||||||
{{ $t('settings-upload-avatar-btn') || 'Change Avatar' }}
|
{{ $t('settings-upload-avatar-btn') || 'Change Avatar' }}
|
||||||
@@ -49,21 +49,25 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { logout as authLogout } from '../store.ts'
|
import { logout as authLogout } from '../store.ts'
|
||||||
import { getAuthData } from "../authStore.ts"
|
import { getAuthData } from "../store.ts"
|
||||||
import type { User } from "../types"
|
import type { User } from "../types"
|
||||||
import UpdateAccountModal from '../components/UpdateAccountModal.vue'
|
import UpdateAccountModal from '../components/UpdateAccountModal.vue'
|
||||||
import { useFluent } from 'fluent-vue'
|
import { useFluent } from 'fluent-vue'
|
||||||
import { saveLocalePreference, getLocalePreference } from "../authStore.ts"
|
import { saveLocalePreference, getLocalePreference } from "../store.ts"
|
||||||
import { getSupportedLanguagesMetadata, setLanguage } from '../i18n'
|
import { getSupportedLanguagesMetadata, setLanguage } from '../i18n'
|
||||||
import UploadAvatarModal from '../components/UploadAvatarModal.vue'
|
import UploadAvatarModal from '../components/UploadAvatarModal.vue'
|
||||||
import defaultAvatar from '../assets/default-avatar.png'
|
import defaultAvatar from '../assets/default-avatar.png'
|
||||||
import { getAvatar } from '../api/account.ts'
|
import { getAvatarUrl } from '../store.ts'
|
||||||
|
|
||||||
const handleAvatarError = (event: Event) => {
|
const handleAvatarError = (event: Event) => {
|
||||||
const img = event.target as HTMLImageElement;
|
const img = event.target as HTMLImageElement;
|
||||||
img.src = defaultAvatar;
|
img.src = defaultAvatar;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleAvatarUpdated() {
|
||||||
|
await fetchUserData()
|
||||||
|
}
|
||||||
|
|
||||||
const showAvatarModal = ref(false)
|
const showAvatarModal = ref(false)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
111
src/store.ts
111
src/store.ts
@@ -1,15 +1,90 @@
|
|||||||
import { apiFetch } from './api/client'
|
import { apiFetch } from './api/client'
|
||||||
import type { LoginResponse, User } from './types'
|
import type { LoginResponse, User } from './types'
|
||||||
import * as authStore from './authStore'
|
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { fetchFriendRequests } from './api/friends'
|
import { fetchFriendRequests } from './api/friends'
|
||||||
import { fetchRoomInvites } from './api/rooms'
|
import { fetchRoomInvites } from './api/rooms'
|
||||||
import type { FriendRequest, RoomInvite } from './types'
|
import type { FriendRequest, RoomInvite } from './types'
|
||||||
import { getAvatar } from './api/account'
|
import { getAvatar } from './api/account'
|
||||||
|
import { load, Store } from '@tauri-apps/plugin-store'
|
||||||
|
import { UpdateUserResponse } from './types'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
let store: Store | null = null
|
||||||
|
export const initAuth = getAuthData
|
||||||
|
|
||||||
|
async function getStore() {
|
||||||
|
if (!store) store = await load('store.json')
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuthData() {
|
||||||
|
const s = await getStore()
|
||||||
|
const token = await s.get<string>('token')
|
||||||
|
const user = await s.get<User>('user')
|
||||||
|
return { token: token || null, user: user || null, isAuthenticated: !!token }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveAuthData(token: string, user: User) {
|
||||||
|
const s = await getStore()
|
||||||
|
await s.set('token', token)
|
||||||
|
await s.set('user', user)
|
||||||
|
await s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearAuthData() {
|
||||||
|
const s = await getStore()
|
||||||
|
s.clear()
|
||||||
|
await s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLocalUser(newData: UpdateUserResponse, uuid?: string) {
|
||||||
|
const updatedUser = {
|
||||||
|
username: newData.username,
|
||||||
|
email: newData.email,
|
||||||
|
uuid,
|
||||||
|
avatar_url: `${getAvatar(uuid || '')}?t=${Date.now()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = await getStore()
|
||||||
|
await s.set('user', updatedUser)
|
||||||
|
await s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshLocalUser() {
|
||||||
|
const s = await getStore()
|
||||||
|
const user = await s.get<User>('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()
|
||||||
|
await s.set('last_room_uuid', uuid)
|
||||||
|
await s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLastRoom(): Promise<string | null> {
|
||||||
|
const s = await getStore()
|
||||||
|
return (await s.get<string>('last_room_uuid')) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveLocalePreference(locale: string) {
|
||||||
|
const s = await getStore()
|
||||||
|
await s.set('language', locale)
|
||||||
|
await s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocalePreference(): Promise<string | null> {
|
||||||
|
const s = await getStore()
|
||||||
|
return (await s.get<string>('language')) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
export const initAuth = authStore.getAuthData
|
|
||||||
export const getLastRoom = authStore.getLastRoom
|
|
||||||
export const setLastRoom = authStore.setLastRoom
|
|
||||||
|
|
||||||
export async function login(email: string, username: string, password: string) {
|
export async function login(email: string, username: string, password: string) {
|
||||||
const res: LoginResponse = await apiFetch('/login', {
|
const res: LoginResponse = await apiFetch('/login', {
|
||||||
@@ -21,19 +96,19 @@ export async function login(email: string, username: string, password: string) {
|
|||||||
uuid: res.uuid,
|
uuid: res.uuid,
|
||||||
username: res.username,
|
username: res.username,
|
||||||
email: res.email,
|
email: res.email,
|
||||||
avatar_url: await getAvatar(res.uuid)
|
avatar_url: getAvatar(res.uuid)
|
||||||
};
|
};
|
||||||
await authStore.saveAuthData(res.token, user)
|
await saveAuthData(res.token, user)
|
||||||
return { token: res.token, uuid: res.uuid, isAuthenticated: true }
|
return { token: res.token, uuid: res.uuid, isAuthenticated: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
await authStore.clearAuthData()
|
await clearAuthData()
|
||||||
return { token: null, uuid: null, isAuthenticated: false }
|
return { token: null, uuid: null, isAuthenticated: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateToken(): Promise<boolean> {
|
export async function validateToken(): Promise<boolean> {
|
||||||
const auth = await authStore.getAuthData()
|
const auth = await getAuthData()
|
||||||
if (!auth.token) return false
|
if (!auth.token) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +137,7 @@ export function useNotifications() {
|
|||||||
const totalCount = computed(() => requests.value.length + invites.value.length)
|
const totalCount = computed(() => requests.value.length + invites.value.length)
|
||||||
|
|
||||||
async function refreshNotifications() {
|
async function refreshNotifications() {
|
||||||
const auth = await authStore.getAuthData()
|
const auth = await getAuthData()
|
||||||
if (!auth.token) {
|
if (!auth.token) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,3 +161,21 @@ export function useNotifications() {
|
|||||||
refreshNotifications
|
refreshNotifications
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A reactive object to store the last updated timestamp for each user
|
||||||
|
const avatarTimestamps = reactive<Record<string, number>>({})
|
||||||
|
|
||||||
|
// Generates the avatar URL with a timestamp
|
||||||
|
export function getAvatarUrl(uuid: string | undefined | null) {
|
||||||
|
if (!uuid) return ''
|
||||||
|
|
||||||
|
if (!avatarTimestamps[uuid]) {
|
||||||
|
avatarTimestamps[uuid] = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${getAvatar(uuid)}?t=${avatarTimestamps[uuid]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshAvatar(uuid: string) {
|
||||||
|
avatarTimestamps[uuid] = Date.now()
|
||||||
|
}
|
||||||
|
|||||||
@@ -409,6 +409,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@tauri-apps/api" "^2.8.0"
|
"@tauri-apps/api" "^2.8.0"
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-fs@~2":
|
||||||
|
version "2.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz#4b32b6de32e2ee735632bff356fab09fcc281b42"
|
||||||
|
integrity sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==
|
||||||
|
dependencies:
|
||||||
|
"@tauri-apps/api" "^2.8.0"
|
||||||
|
|
||||||
"@tauri-apps/plugin-http@~2":
|
"@tauri-apps/plugin-http@~2":
|
||||||
version "2.5.4"
|
version "2.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.5.4.tgz#998a9cd02efa006fcbeddd92e8e51434ff3804dd"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.5.4.tgz#998a9cd02efa006fcbeddd92e8e51434ff3804dd"
|
||||||
|
|||||||
Reference in New Issue
Block a user