refined colors, added multiple color themes, and added window control titlebar

This commit is contained in:
2026-01-18 20:31:40 +01:00
parent a4c6e0661b
commit 4ff3e30348
28 changed files with 526 additions and 117 deletions

View File

@@ -1,16 +1,35 @@
<template>
<div id="page">
<main id="content">
<router-view />
</main>
<VersionWarningModal v-if="showVersionWarningModal" :appVersion="appVersion" :backendVersion="backendVersion"
:expectedBackendVersion="expectedBackendVersion" @close="showVersionWarningModal = false" />
<footer v-if="!$route.meta.hideNavbar">
<Navbar />
</footer>
<div id="page">
<div v-if="currentPlatform != 'android' && currentPlatform != 'ios'" data-tauri-drag-region class="titlebar">
<div class="titlebar-button" @click="minimize">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
</svg>
</div>
<div class="titlebar-button" @click="toggleMaximize">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect fill="none" stroke="currentColor" stroke-width="1" width="9" height="9" x="1.5" y="1.5" />
</svg>
</div>
<div class="titlebar-button" id="close-btn" @click="close">
<svg width="12" height="12" viewBox="0 0 12 12">
<path fill="currentColor"
d="M11 1.57L10.43 1 6 5.43 1.57 1 1 1.57 5.43 6 1 10.43 1.57 11 6 6.57 10.43 11 11 10.43 6.57 6z" />
</svg>
</div>
</div>
<main id="content">
<router-view />
</main>
<VersionWarningModal v-if="showVersionWarningModal" :appVersion="appVersion" :backendVersion="backendVersion"
:expectedBackendVersion="expectedBackendVersion" @close="showVersionWarningModal = false" />
<footer v-if="!$route.meta.hideNavbar">
<Navbar />
</footer>
</div>
</template>
<script setup lang="ts">
@@ -19,6 +38,12 @@ import Navbar from './components/Navbar.vue'
import VersionWarningModal from './components/VersionWarningModal.vue'
import { apiFetch } from './api/client'
import { VersionResponse } from './types'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { platform } from '@tauri-apps/plugin-os';
const currentPlatform = ref('')
const appWindow = getCurrentWindow()
const showVersionWarningModal = ref(false)
const backendVersion = ref('')
@@ -26,52 +51,97 @@ const backendVersion = ref('')
const appVersion = __APP_VERSION__
const expectedBackendVersion = __BACKEND_VERSION__
const isFullScreen = ref(false)
onMounted(async () => {
backendVersion.value = (await getBackendVersion()).version
if (backendVersion.value !== expectedBackendVersion) {
showVersionWarningModal.value = true
}
backendVersion.value = (await getBackendVersion()).version
if (backendVersion.value !== expectedBackendVersion) {
showVersionWarningModal.value = true
}
currentPlatform.value = platform()
})
const minimize = () => appWindow.minimize()
const toggleMaximize = () => {
appWindow.setFullscreen(!isFullScreen.value)
isFullScreen.value = !isFullScreen.value
}
const close = () => appWindow.close()
async function getBackendVersion() {
return await apiFetch<VersionResponse>('/version')
return await apiFetch<VersionResponse>('/version')
}
</script>
<style scoped>
#page {
background: var(--bg);
height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg);
height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
}
#content {
width: 100%;
max-width: 1100px;
padding: 2rem;
width: 100%;
max-width: 1100px;
/* padding: 2rem; */
padding: calc(20px + 2rem) 2rem 2rem 2rem;
flex: 1;
overflow-y: auto;
flex: 1;
overflow-y: auto;
}
.titlebar {
height: 30px;
background: var(--panel);
user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
}
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 45px;
height: 30px;
transition: background-color 0.2s;
color: var(--text-color);
cursor: pointer;
}
.titlebar-button:hover {
background: var(--panel-accent)
}
#close-btn:hover {
background: #e81123;
color: white;
}
footer {
width: 100%;
display: flex;
justify-content: center;
padding-bottom: 24px;
background: var(--bg);
width: 100%;
display: flex;
justify-content: center;
padding-bottom: 24px;
background: var(--bg);
}
@media (max-width: 720px) {
#content {
padding: 12px;
padding-top: 30px;
}
#content {
padding: 12px;
padding-top: 30px;
}
footer {
padding-bottom: 56px;
}
footer {
padding-bottom: 56px;
}
}
</style>

View File

@@ -16,6 +16,7 @@ body,
overflow-y: hidden;
}
/* This is overwritten by the theme configuration */
:root {
--bg: #0f1116;
--panel: #171922;
@@ -23,6 +24,7 @@ body,
--text: #e6e6eb;
--muted: #9aa0aa;
--accent: #f27aa3;
--accent-rgb: 242, 122, 163;
--accent-hover: #ff91b3;
--accent-second: #96CDFB;
--border: #2a2f3b;
@@ -30,6 +32,10 @@ body,
--radius: 8px;
}
* {
transition: background-color 0.2s ease, border-color 0.2s ease;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
@@ -59,7 +65,8 @@ input:focus, textarea:focus {
button, .button {
font-size: 1rem;
background: var(--accent);
color: #0b0d12;
color: var(--bg);
/* color: var(--btn-text); */
border: none;
border-radius: var(--radius);
padding: 0.6rem 1rem;
@@ -74,6 +81,19 @@ button:hover, .button:hover {
background: var(--accent-hover);
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
padding: 8px 16px;
border-radius: var(--radius);
cursor: pointer;
}
.secondary:hover {
background: var(--panel-hover);
}
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;

View File

@@ -347,7 +347,7 @@ async function onSend(content: string) {
border-radius: var(--radius);
border: none;
background: transparent;
color: white;
color: var(--text);
cursor: pointer;
font-size: 1.6rem;
@@ -386,7 +386,7 @@ async function onSend(content: string) {
}
.retry-btn:hover {
background: rgba(255, 255, 255, 0.05);
background: var(--panel-hover);
}
.no-room {

View File

@@ -76,15 +76,6 @@ const emit = defineEmits(['yes', 'no']);
color: var(--text);
}
.secondary {
cursor: pointer;
padding: 8px 16px;
border-radius: var(--radius);
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn-danger {
border-color: var(--error);
color: var(--error);

View File

@@ -92,14 +92,4 @@ async function submit() {
justify-content: flex-end;
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.secondary:hover {
background: rgba(255, 255, 255, 0.05);
}
</style>

View File

@@ -120,14 +120,4 @@ async function submit() {
justify-content: flex-end;
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.secondary:hover {
background: rgba(255, 255, 255, 0.05);
}
</style>

View File

@@ -76,7 +76,7 @@ onMounted(() => {
top: 4px;
right: 4px;
background-color: var(--accent);
color: black;
color: var(--bg);
font-size: 0.65rem;
font-weight: bold;
min-width: 18px;
@@ -92,7 +92,7 @@ onMounted(() => {
@media (hover: hover) {
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
background: var(--panel-hover);
}
.nav-item:not(.router-link-active):hover i {

View File

@@ -205,15 +205,6 @@ const handleAvatarError = (event: Event) => {
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
padding: 8px 16px;
border-radius: var(--radius);
cursor: pointer;
}
.member-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));

View File

@@ -138,7 +138,7 @@ onMounted(async () => {
}
.retry-btn:hover {
background: rgba(255, 255, 255, 0.05);
background: var(--panel-hover);
}
.room-content-wrapper {
@@ -150,7 +150,7 @@ onMounted(async () => {
.unread-badge {
background-color: var(--accent);
color: black;
color: var(--bg);
font-size: 0.75rem;
font-weight: bold;
min-width: 20px;
@@ -193,7 +193,7 @@ onMounted(async () => {
}
.room-item:hover {
background: rgba(255, 255, 255, 0.05);
background: var(--panel-hover);
color: var(--text);
}
@@ -236,6 +236,6 @@ onMounted(async () => {
}
.create-btn:hover {
background: rgba(255, 255, 255, 0.05);
background: var(--panel-hover);
}
</style>

View File

@@ -112,7 +112,6 @@ async function submit() {
display: flex;
flex-direction: column;
gap: 1.2rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.subtitle {
@@ -154,10 +153,4 @@ button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
</style>

View File

@@ -202,7 +202,7 @@ async function handleUpload() {
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.02);
background: var(--panel-accent);
min-height: 180px;
display: flex;
align-items: center;
@@ -221,7 +221,7 @@ async function handleUpload() {
.drop-zone:hover {
border-color: var(--accent);
background: rgba(255, 255, 255, 0.05);
background: var(--panel-hover);
}
.drop-content i {
@@ -299,10 +299,4 @@ async function handleUpload() {
justify-content: flex-end;
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
</style>

View File

@@ -49,10 +49,4 @@ const emit = defineEmits(['close']);
justify-content: flex-end;
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
</style>

View File

@@ -79,6 +79,7 @@ settings-loading = Loading settings...
settings-title = Settings
settings-account = Account
settings-language = Language
settings-appearance = Appearance
settings-label-username = Username:
settings-label-email = Email:
settings-update-btn = Update

View File

@@ -79,6 +79,7 @@ settings-loading = Chargement des paramètres...
settings-title = Paramètres
settings-account = Compte
settings-language = Langue
settings-appearance = Apparence
settings-label-username = Nom d'utilisateur :
settings-label-email = Email :
settings-update-btn = Modifier

View File

@@ -1,7 +1,7 @@
import { createApp } from 'vue'
import router from './router.ts'
import App from './App.vue'
import { validateToken } from './store.ts'
import { validateToken, initTheme } from './store.ts'
import { fluent, setLanguage } from './i18n'
import './base.css'
@@ -17,6 +17,8 @@ async function init() {
const savedLocale = await getLocalePreference();
const osLocale = navigator.language;
await initTheme();
if (savedLocale) {
setLanguage(savedLocale);
} else {

View File

@@ -43,7 +43,7 @@ const handleRoomAction = async () => {
await roomListRef.value.refreshRooms();
}
router.push('/rooms/none');
router.push('/rooms/none');
};
const handleNotification = (roomUuid: string) => {
@@ -152,7 +152,7 @@ const handleNotification = (roomUuid: string) => {
.sidebar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.3);
/* background: rgba(0, 0, 0, 0.3); */
z-index: 15;
}

View File

@@ -182,7 +182,7 @@ async function declineRoom(senderUuid: string, roomUuid: string) {
}
.decline-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
background-color: var(--panel-hover);
}
@media (max-width: 720px) {

View File

@@ -38,6 +38,14 @@
</div>
</div>
<h2>{{ $t('settings-appearance') || 'Appearance' }}</h2>
<div class="theme-grid">
<button v-for="theme in availableThemes" :key="theme.id" class="theme-btn"
:class="{ active: currentTheme === theme.id }" @click="changeTheme(theme.id)">
{{ theme.name }}
</button>
</div>
<button class="logout-btn" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
<span>{{ $t('settings-logout-btn') }}</span>
@@ -49,6 +57,7 @@
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { logout as authLogout } from '../store.ts'
import { saveThemePreference, getThemePreference } from "../store.ts"
import { getAuthData } from "../store.ts"
import type { User } from "../types"
import UpdateAccountModal from '../components/UpdateAccountModal.vue'
@@ -58,6 +67,7 @@ import { getSupportedLanguagesMetadata, setLanguage } from '../i18n'
import UploadAvatarModal from '../components/UploadAvatarModal.vue'
import defaultAvatar from '../assets/default-avatar.png'
import { getAvatarUrl } from '../store.ts'
import { getAvailableThemes } from '../themeLoader';
const handleAvatarError = (event: Event) => {
const img = event.target as HTMLImageElement;
@@ -73,6 +83,8 @@ const { $t } = useFluent()
const currentLang = ref('')
const languages = computed(() => getSupportedLanguagesMetadata())
const currentTheme = ref('default');
const availableThemes = getAvailableThemes();
async function fetchUserData() {
try {
@@ -85,8 +97,8 @@ async function fetchUserData() {
onMounted(async () => {
const pref = await getLocalePreference()
// Synchronize the UI state with the actual active language
currentLang.value = pref || (navigator.language.split('-')[0])
currentTheme.value = await getThemePreference()
fetchUserData()
})
@@ -97,6 +109,11 @@ async function changeLanguage(code: string) {
await saveLocalePreference(actual)
}
async function changeTheme(theme: string) {
currentTheme.value = theme
await saveThemePreference(theme)
}
function logout() {
authLogout()
router.push('/login')
@@ -171,12 +188,44 @@ h2 {
margin: 0;
}
.lang-btn:hover:not(.active) {
background: rgba(var(--accent-rgb), 0.1);
border-color: var(--accent);
}
.lang-btn.active {
background: var(--accent);
color: #000;
border-color: var(--accent);
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.theme-btn {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
margin: 0;
padding: 12px;
font-size: 0.9rem;
}
.theme-btn.active {
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
}
.theme-btn:hover:not(.active) {
background: rgba(var(--accent-rgb), 0.1);
border-color: var(--accent);
}
.logout-btn {
cursor: pointer;
display: flex;
@@ -193,11 +242,11 @@ h2 {
}
.logout-btn:hover {
color: rgba(255, 80, 80, 0.8);
color: var(--error);
}
.logout-btn:hover i {
color: rgba(255, 80, 80, 0.8);
color: var(--error);
}
.logout-btn i {

View File

@@ -8,6 +8,7 @@ import { load, Store } from '@tauri-apps/plugin-store'
import { UpdateUserResponse } from './types'
import { reactive } from 'vue'
import { API } from './main'
import { applyTheme } from './themeLoader.ts';
let store: Store | null = null
export const initAuth = getAuthData
@@ -184,3 +185,28 @@ export function getAvatarUrl(uuid: string | undefined | null) {
export function refreshAvatar(uuid: string) {
avatarTimestamps[uuid] = Date.now()
}
// ==== Color themes ====
export async function saveThemePreference(themeId: string) {
const s = await getStore();
await s.set('theme', themeId);
await s.save();
applyTheme(themeId);
}
export async function initTheme() {
const s = await getStore();
const themeId = (await s.get<string>('theme')) || 'default';
applyTheme(themeId);
}
export async function getThemePreference(): Promise<string> {
const s = await getStore()
return (await s.get<string>('theme')) ?? 'default'
}
export async function applyStoredTheme() {
const theme = await getThemePreference();
document.documentElement.setAttribute('data-theme', theme);
}

26
src/themeLoader.ts Normal file
View File

@@ -0,0 +1,26 @@
import themes from './themes.json';
export type ThemeKey = keyof typeof themes;
export function applyTheme(themeKey: string) {
const theme = (themes as any)[themeKey] || themes.default;
const root = document.documentElement;
// Convert json keys to css Variables
Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value as string);
});
// if (themeKey.includes('light')) {
// root.style.setProperty('--btn-text', theme.colors.bg);
// } else {
// root.style.setProperty('--btn-text', '#0b0d12');
// }
}
export function getAvailableThemes() {
return Object.entries(themes).map(([id, data]) => ({
id,
name: data.name
}));
}

172
src/themes.json Normal file
View File

@@ -0,0 +1,172 @@
{
"frangipane-dark": {
"name": "Frangipane Dark",
"colors": {
"bg": "#0f1116",
"panel": "#171922",
"panel-accent": "#12141B",
"panel-hover": "#22242D",
"text": "#e6e6eb",
"muted": "#9aa0aa",
"accent": "#f27aa3",
"accent-rgb": "242, 122, 163",
"accent-hover": "#ff91b3",
"accent-second": "#96CDFB",
"border": "#2a2f3b",
"error": "#ff5050"
}
},
"catppuccin-latte": {
"name": "Catppuccin Latte",
"colors": {
"bg": "#eff1f5",
"panel": "#e6e9ef",
"panel-hover": "#bcc0cc",
"panel-accent": "#ccd0da",
"text": "#4c4f69",
"muted": "#7c7f93",
"accent": "#ea76cb",
"accent-rgb": "234, 118, 203",
"accent-hover": "#d20f39",
"accent-second": "#1e66f5",
"border": "#dce0e8",
"error": "#d20f39"
}
},
"catppuccin-frappe": {
"name": "Catppuccin Frappé",
"colors": {
"bg": "#303446",
"panel": "#292c3c",
"panel-accent": "#232634",
"panel-hover": "#414559",
"text": "#c6d0f5",
"muted": "#838ba7",
"accent": "#a6d189",
"accent-rgb": "166, 209, 137",
"accent-hover": "#81c8be",
"accent-second": "#f2d5cf",
"border": "#414559",
"error": "#e78284"
}
},
"catppuccin-macchiato": {
"name": "Catppuccin Macchiato",
"colors": {
"bg": "#24273a",
"panel": "#1e2030",
"panel-accent": "#181926",
"panel-hover": "#363a4f",
"text": "#cad3f5",
"muted": "#8087a2",
"accent": "#c6a0f6",
"accent-rgb": "198, 160, 246",
"accent-hover": "#f5bde6",
"accent-second": "#8aadf4",
"border": "#494d64",
"error": "#ed8796"
}
},
"catppuccin-mocha": {
"name": "Catppuccin Mocha",
"colors": {
"bg": "#1e1e2e",
"panel": "#181825",
"panel-accent": "#11111b",
"panel-hover": "#313244",
"text": "#cdd6f4",
"muted": "#7f849c",
"accent": "#89b4fa",
"accent-rgb": "137, 180, 250",
"accent-hover": "#89dceb",
"accent-second": "#f5c2e7",
"border": "#313244",
"error": "#f38ba8"
}
},
"nord": {
"name": "Nordic",
"colors": {
"bg": "#2e3440",
"panel": "#3b4252",
"panel-accent": "#242933",
"panel-hover": "#434c5e",
"text": "#eceff4",
"muted": "#d8dee9",
"accent": "#88c0d0",
"accent-rgb": "136, 192, 208",
"accent-hover": "#8fbcbb",
"accent-second": "#81a1c1",
"border": "#4c566a",
"error": "#bf616a"
}
},
"tokyo-night": {
"name": "Tokyo Night",
"colors": {
"bg": "#1a1b26",
"panel": "#16161e",
"panel-accent": "#1f2335",
"panel-hover": "#292e42",
"text": "#a9b1d6",
"muted": "#565f89",
"accent": "#7aa2f7",
"accent-rgb": "122, 162, 247",
"accent-hover": "#7dcfff",
"accent-second": "#bb9af7",
"border": "#24283b",
"error": "#f7768e"
}
},
"gruvbox-dark": {
"name": "Gruvbox Dark",
"colors": {
"bg": "#282828",
"panel": "#3c3836",
"panel-accent": "#282828",
"panel-hover": "#504945",
"text": "#ebdbb2",
"muted": "#a89984",
"accent": "#fabd2f",
"accent-rgb": "250, 189, 47",
"accent-hover": "#fe8019",
"accent-second": "#b8bb26",
"border": "#504945",
"error": "#fb4934"
}
},
"gruvbox-light": {
"name": "Gruvbox Light",
"colors": {
"bg": "#fbf1c7",
"panel": "#f2e5bc",
"panel-accent": "#d5c4a1",
"panel-hover": "#ebdbb2",
"text": "#3c3836",
"muted": "#7c6f64",
"accent": "#d65d0e",
"accent-rgb": "214, 93, 14",
"accent-hover": "#9d0006",
"accent-second": "#458588",
"border": "#bdae93",
"error": "#cc241d"
}
},
"solarized-dark": {
"name": "Solarized Dark",
"colors": {
"bg": "#002b36",
"panel": "#073642",
"panel-accent": "#00212b",
"panel-hover": "#586e75",
"text": "#839496",
"muted": "#657b83",
"accent": "#268bd2",
"accent-rgb": "38, 139, 210",
"accent-hover": "#2aa198",
"accent-second": "#859900",
"border": "#073642",
"error": "#dc322f"
}
}
}